diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 831df2d..e8b63bb 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -7,7 +7,7 @@ body:
attributes:
value: |
Thanks for taking the time to report a bug! Please fill out the information below.
-
+
- type: textarea
id: description
attributes:
@@ -16,7 +16,7 @@ body:
placeholder: What happened?
validations:
required: true
-
+
- type: textarea
id: reproduction
attributes:
@@ -28,7 +28,7 @@ body:
3. See error...
validations:
required: true
-
+
- type: textarea
id: expected
attributes:
@@ -36,7 +36,7 @@ body:
description: What did you expect to happen?
validations:
required: true
-
+
- type: textarea
id: environment
attributes:
@@ -48,7 +48,7 @@ body:
- miniloom version:
validations:
required: true
-
+
- type: textarea
id: additional
attributes:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 74144dc..dee01c0 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -7,7 +7,7 @@ body:
attributes:
value: |
Thanks for suggesting a feature! Please describe what you'd like to see.
-
+
- type: textarea
id: problem
attributes:
@@ -16,7 +16,7 @@ body:
placeholder: I'm trying to do X but currently...
validations:
required: true
-
+
- type: textarea
id: solution
attributes:
@@ -25,13 +25,13 @@ body:
placeholder: I'd like miniloom to...
validations:
required: true
-
+
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Any alternative solutions or workarounds you've considered?
-
+
- type: textarea
id: additional
attributes:
diff --git a/.github/ISSUE_TEMPLATE/general.yml b/.github/ISSUE_TEMPLATE/general.yml
index a21149d..5de0401 100644
--- a/.github/ISSUE_TEMPLATE/general.yml
+++ b/.github/ISSUE_TEMPLATE/general.yml
@@ -10,7 +10,7 @@ body:
description: Describe your issue, question, or concern
validations:
required: true
-
+
- type: textarea
id: context
attributes:
diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md
new file mode 100644
index 0000000..45fc99a
--- /dev/null
+++ b/.github/pull-request-template.md
@@ -0,0 +1,32 @@
+## What does this PR do?
+
+Brief description of the changes.
+
+## Type of change
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] Documentation update
+- [ ] Code cleanup/refactoring
+
+## Testing
+
+- [ ] Tested locally with `npm start`
+- [ ] Tested text generation with OpenRouter
+- [ ] Code formatted with `npm run format`
+- [ ] Built successfully with `npm run build` (specify platform: mac/win/linux)
+
+## Checklist
+
+- [ ] My code follows the project's style guidelines
+- [ ] I have performed a self-review of my own code
+- [ ] My changes generate no new warnings or errors
+
+## Additional context
+
+Add any other context about the pull request here.
+
+---
+
+š¬ **Questions?** Ask in our [Discord](https://discord.gg/Y3HGwrcPwr)
diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml
new file mode 100644
index 0000000..2e6eac3
--- /dev/null
+++ b/.github/workflows/build-release.yaml
@@ -0,0 +1,76 @@
+name: Build and Release
+
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os: [macos-latest, ubuntu-latest, windows-latest]
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "18"
+ cache: "npm"
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build for macOS
+ if: matrix.os == 'macos-latest'
+ run: npm run build:mac
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build for Windows
+ if: matrix.os == 'windows-latest'
+ run: npm run build:win
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build for Linux
+ if: matrix.os == 'ubuntu-latest'
+ run: npm run build:linux
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.os }}-build
+ path: dist/
+ retention-days: 7
+
+ release:
+ needs: build
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: ./artifacts
+
+ - name: Create Release
+ uses: softprops/action-gh-release@v1
+ with:
+ files: ./artifacts/**/*
+ draft: false
+ prerelease: false
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 17cda63..e8764e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,11 @@
node_modules/
-local/
\ No newline at end of file
+local/
+.DS_Store
+
+# Build outputs
+dist/
+build/
+out/
+
+# Electron builder cache
+node_modules/.cache/
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..c3bc019
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,14 @@
+# Prompts
+prompts/
+
+# Dependencies
+node_modules/
+package-lock.json
+
+# Build outputs
+dist/
+build/
+out/
+
+# Logs
+*.log
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..25395de
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,17 @@
+{
+ "semi": true,
+ "trailingComma": "es5",
+ "singleQuote": false,
+ "printWidth": 80,
+ "tabWidth": 2,
+ "useTabs": false,
+ "bracketSpacing": true,
+ "bracketSameLine": false,
+ "arrowParens": "avoid",
+ "endOfLine": "lf",
+ "quoteProps": "as-needed",
+ "jsxSingleQuote": false,
+ "proseWrap": "preserve",
+ "htmlWhitespaceSensitivity": "css",
+ "embeddedLanguageFormatting": "auto"
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a53a4d5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2023, John David Pressman and Katherine Crowson
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7b814a7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,108 @@
+
+
+ MiniLoom
+
+
+MiniLoom is a desktop application for AI text generation.
+
+## Features
+
+- Desktop application with native UI
+- Text generation capabilities from a variety of LLM APIs (supports OpenAI Completions and Chat formats, you specify the URL; verified to work with Together, OpenRouter, VLLM, OpenAI, X AI, Google AI, Anthropic)
+- File save/load functionality
+- Settings management
+- Text search functionality
+
+## Getting Started
+
+### First Time Setup
+
+When you first open MiniLoom, the app will automatically detect that you're a new user and open the Settings panel with a welcome message. Here's what you need to do to get started:
+
+1. **Create a Service** (Required)
+ - Click on the "š Services" tab
+ - Click "Add New" to create your first service
+ - Choose a service type (OpenRouter is recommended for beginners)
+ - Fill in the required details:
+ - **Service Name**: Give it a memorable name (e.g., "My OpenRouter")
+ - **API URL**: Usually pre-filled for popular services
+ - **Model Name**: Choose a model (e.g., "deepseek/deepseek-v3-base:free" for OpenRouter)
+ - Click "Save"
+
+2. **Add an API Key** (Required for most services)
+ - Click on the "š API Keys" tab
+ - Enter a name for your key (e.g., "OPENROUTER_KEY")
+ - Paste your API key in the "Secret" field
+ - Click "Add"
+ - **Note**: Some APIs may work without a key for free tiers
+
+3. **Configure Sampling** (Optional - Default provided)
+ - Click on the "š² Samplers" tab
+ - A "Default" sampler is automatically created for new users
+ - You can customize it or create new ones as needed
+
+### Using the App
+
+1. **Select Your Configuration**
+ - In the bottom control bar, select your Service, API Key, and Sampler
+ - The order is: Service ā Key ā Sampler
+
+2. **Start Generating**
+ - Type your prompt in the main editor
+ - Click the "šļø Generate" button
+ - Your text will be generated and added to the tree structure
+
+3. **Navigate and Expand**
+ - Use the tree view on the left to navigate between different generations
+ - Click on any node to focus on it and continue from that point
+ - Use thumbs up/down to rate responses
+
+## Getting Help
+
+- š¬ **Questions & Discussion**: Join our [Discord](https://discord.gg/Y3HGwrcPwr)
+- š **Bug Reports**: Use GitHub issues (confirmed bugs only)
+- š” **Feature Ideas**: Discuss in Discord first, then create a GitHub issue
+
+## Prerequisites
+
+- Node.js / npm (version 14 or higher)
+
+## Installation
+
+1. Clone the repository:
+
+```bash
+git clone https://github.com/JD-P/miniloom.git
+cd miniloom
+```
+
+2. Install dependencies:
+
+```bash
+npm install
+```
+
+## Usage
+
+To start the application:
+
+```bash
+npm start
+```
+
+This will launch the MiniLoom desktop application.
+
+## Development
+
+The application is built with:
+
+- Electron for the desktop framework
+- Vanilla JavaScript for the frontend
+- MiniSearch for search functionality
+- Diff-match-patch for text comparison
+
+## License
+
+Apache License 2.0
+
+This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
diff --git a/assets/icon.icns b/assets/icon.icns
new file mode 100644
index 0000000..6906198
Binary files /dev/null and b/assets/icon.icns differ
diff --git a/assets/icon.ico b/assets/icon.ico
new file mode 100644
index 0000000..b086686
Binary files /dev/null and b/assets/icon.ico differ
diff --git a/index.html b/index.html
deleted file mode 100644
index b9dfe3c..0000000
--- a/index.html
+++ /dev/null
@@ -1,704 +0,0 @@
-
-
-
-
- MiniLoom
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/main.js b/main.js
deleted file mode 100644
index c34ae15..0000000
--- a/main.js
+++ /dev/null
@@ -1,229 +0,0 @@
-const {
- app,
- BrowserWindow,
- ipcMain,
- dialog,
- Menu,
- MenuItem,
-} = require("electron");
-const fs = require("fs");
-const path = require("path");
-
-let mainWindow;
-
-function createWindow() {
- mainWindow = new BrowserWindow({
- title: "MiniLoom",
- icon: path.join(__dirname, "assets/minihf_logo_no_text.png"),
- width: 800,
- height: 600,
- webPreferences: {
- nodeIntegration: true,
- contextIsolation: false,
- },
- });
-
- // Get the existing menu template
- const existingMenuTemplate = Menu.getApplicationMenu().items.map((item) => {
- return {
- label: item.label,
- submenu: item.submenu.items,
- };
- });
-
- // Define new items for the File menu
- const fileMenuItems = [
- {
- label: "Save",
- accelerator: "CmdOrCtrl+S",
- click() {
- mainWindow.webContents.send("invoke-action", "save-file");
- },
- },
- {
- label: "Load",
- accelerator: "CmdOrCtrl+O",
- click() {
- mainWindow.webContents.send("invoke-action", "load-file");
- },
- },
- { type: "separator" }, // Separator
- ];
-
- // Find the File menu in the existing template
- const fileMenuIndex = existingMenuTemplate.findIndex(
- (item) => item.label === "File"
- );
-
- if (fileMenuIndex >= 0) {
- // If File menu exists, append new items to it
- existingMenuTemplate[fileMenuIndex].submenu = fileMenuItems.concat(
- existingMenuTemplate[fileMenuIndex].submenu
- );
- } else {
- // If File menu doesn't exist, add it
- existingMenuTemplate.unshift({
- label: "File",
- submenu: fileMenuItems,
- });
- }
-
- const editMenuItems = [
- {
- label: "Settings",
- accelerator: "CmdOrCtrl+P",
- click: openSettingsWindow,
- },
- ];
-
- const editMenuIndex = existingMenuTemplate.findIndex(
- (item) => item.label === "Edit"
- );
-
- if (editMenuIndex >= 0) {
- existingMenuTemplate[editMenuIndex].submenu = [
- ...existingMenuTemplate[editMenuIndex].submenu,
- {type: "separator" },
- ...editMenuItems,
- ];
- }
-
- // Build and set the new menu
- const newMenu = Menu.buildFromTemplate(existingMenuTemplate);
- Menu.setApplicationMenu(newMenu);
-
- mainWindow.loadFile("index.html");
-
- mainWindow.on("closed", function () {
- mainWindow = null;
- });
-}
-
-function openSettingsWindow() {
- const modal = new BrowserWindow({
- parent: mainWindow,
- modal: true,
- show: false,
- width: 400,
- height: 300,
- webPreferences: {
- nodeIntegration: true,
- contextIsolation: false,
- }
- });
-
- modal.loadFile('settings.html');
- modal.once('ready-to-show', () => modal.show());
-
- modal.on('closed', () => {
- // Inform the main window to refresh its UI
- mainWindow.webContents.send('settings-updated');
- });
-}
-
-// Listen for open-settings request
-ipcMain.handle('open-settings', openSettingsWindow);
-
-let autoSavePath = null;
-
-ipcMain.handle("save-file", async (event, data) => {
- let filePath;
- if (autoSavePath) {
- filePath = autoSavePath;
- } else {
- const { filePath: chosenPath } = await dialog.showSaveDialog(mainWindow, {
- title: "Save File",
- filters: [{ name: "JSON Files", extensions: ["json"] }],
- });
- filePath = chosenPath;
- autoSavePath = chosenPath; // Update auto-save path
- }
-
- if (filePath) {
- fs.writeFileSync(filePath, JSON.stringify(data));
- }
-});
-
-ipcMain.handle("load-file", async (event) => {
- const { filePaths } = await dialog.showOpenDialog(mainWindow, {
- title: "Load File",
- filters: [{ name: "JSON Files", extensions: ["json"] }],
- properties: ["openFile"],
- });
-
- if (filePaths && filePaths.length > 0) {
- const content = fs.readFileSync(filePaths[0], "utf8");
- autoSavePath = filePaths[0]; // Update auto-save path
- return JSON.parse(content);
- }
-});
-
-ipcMain.handle("load-settings", async (event) => {
- const miniLoomSettingsFilePath = path.join(
- app.getPath("appData"),
- "miniloom",
- "settings.json"
- );
- let settings;
- if (fs.existsSync(miniLoomSettingsFilePath)) {
- settings = fs.readFileSync(miniLoomSettingsFilePath, "utf8");
- return JSON.parse(settings);
- }
-});
-
-function saveSettingsSync(miniLoomSettings) {
- const appDataPath = app.getPath("appData");
- const miniLoomSettingsDir = path.join(appDataPath, "miniloom");
- const miniLoomSettingsFilePath = path.join(
- miniLoomSettingsDir,
- "settings.json"
- );
- if (!fs.existsSync(miniLoomSettingsDir)) {
- fs.mkdirSync(miniLoomSettingsDir);
- }
- fs.writeFileSync(miniLoomSettingsFilePath, JSON.stringify(miniLoomSettings));
-}
-
-ipcMain.handle("auto-save", (event, data) => {
- const userFileData = {};
- userFileData["loomTree"] = data["loomTree"];
- userFileData["focus"] = data["focus"];
- if (autoSavePath) {
- fs.writeFileSync(autoSavePath, JSON.stringify(userFileData));
- }
- saveSettingsSync(data["samplerSettingsStore"]);
-});
-
-ipcMain.handle("save-settings", (event, miniLoomSettings) => {
- saveSettingsSync(miniLoomSettings);
-});
-
-app
- .whenReady()
- .then(() => {
- app.setName("MiniLoom");
- if (process.platform === "darwin") {
- app.dock.setIcon(path.join(__dirname, "assets/minihf_logo_no_text.png"));
- }
- })
- .then(createWindow);
-
-app.on("window-all-closed", function () {
- if (process.platform !== "darwin") app.quit();
-});
-
-app.on("activate", function () {
- if (mainWindow === null) createWindow();
-});
-
-ipcMain.on("show-context-menu", (event) => {
- const contextMenu = Menu.buildFromTemplate([
- { label: "Cut", role: "cut" },
- { label: "Copy", role: "copy" },
- { label: "Paste", role: "paste" },
- { type: "separator" },
- { label: "Select All", role: "selectAll" },
- ]);
-
- contextMenu.popup(BrowserWindow.fromWebContents(event.sender));
-});
diff --git a/minisearch-integration.js b/minisearch-integration.js
deleted file mode 100644
index cf1cffe..0000000
--- a/minisearch-integration.js
+++ /dev/null
@@ -1,720 +0,0 @@
-// Add this to the top of renderer.js after the existing imports
-// Note: You'll need to include MiniSearch via CDN or npm install
-
-// Global search index variable
-let searchIndex = null;
-let searchResultsMode = false;
-let currentSearchQuery = "";
-
-// Initialize search index with MiniSearch
-function initializeSearchIndex() {
- // Create MiniSearch index - much simpler than Lunr!
- searchIndex = new MiniSearch({
- fields: ["content", "summary", "type"], // fields to index for full-text search
- storeFields: ["content", "summary", "type", "timestamp", "fullContent"], // fields to return with search results
- searchOptions: {
- boost: {
- content: 3, // Patch content gets highest boost
- summary: 2, // Summary gets medium boost
- type: 1, // Node type gets lower boost
- },
- prefix: true, // Enable prefix search (great for real-time search)
- fuzzy: 0.2, // Enable fuzzy search for typos
- },
- });
-
- // Add all existing nodes to the index
- Object.keys(loomTree.nodeStore).forEach((nodeId) => {
- addNodeToSearchIndex(loomTree.nodeStore[nodeId]);
- });
-
- console.log(
- `MiniSearch index initialized with ${
- Object.keys(loomTree.nodeStore).length
- } nodes`
- );
-}
-
-// Extract meaningful content from diff patches
-function extractPatchContent(patch) {
- if (!patch || typeof patch === "string") {
- return patch || "";
- }
-
- try {
- // If patch is a DiffMatchPatch patch array
- if (Array.isArray(patch)) {
- return patch
- .map((p) => {
- if (p.diffs) {
- return p.diffs
- .filter((diff) => diff[0] === 1) // Only get insertions (new content)
- .map((diff) => diff[1])
- .join(" ");
- }
- return "";
- })
- .join(" ");
- }
-
- return "";
- } catch (error) {
- console.warn("Error extracting patch content:", error);
- return "";
- }
-}
-
-// Add a single node to the search index
-function addNodeToSearchIndex(node) {
- if (!searchIndex || !node) return;
-
- const patchContent = extractPatchContent(node.patch);
- if (!patchContent.trim() && !node.summary) return; // Skip nodes with no searchable content
-
- try {
- searchIndex.add({
- id: node.id,
- content: patchContent,
- summary: node.summary || "",
- type: node.type,
- timestamp: node.timestamp,
- fullContent: loomTree.renderNode(node), // Store for display
- });
- } catch (error) {
- console.warn("Error adding node to search index:", error);
- }
-}
-
-// Remove a node from the search index
-function removeNodeFromSearchIndex(node) {
- if (!searchIndex) return;
-
- try {
- searchIndex.remove(node);
- } catch (error) {
- console.warn("Error removing node from search index:", error);
- }
-}
-
-// Update a node in the search index
-function updateNodeInSearchIndex(node) {
- if (!searchIndex || !node) return;
-
- try {
- searchIndex.replace({
- id: node.id,
- content: extractPatchContent(node.patch),
- summary: node.summary || "",
- type: node.type,
- timestamp: node.timestamp,
- fullContent: loomTree.renderNode(node),
- });
- } catch (error) {
- console.warn("Error updating node in search index:", error);
- }
-}
-
-// Perform search with MiniSearch
-function performSearch(query) {
- if (!searchIndex || !query.trim()) {
- return [];
- }
-
- try {
- const results = searchIndex.search(query, {
- // MiniSearch allows per-query options
- boost: {
- content: 3,
- summary: 2,
- type: 1,
- },
- prefix: true,
- fuzzy: 0.2,
- });
-
- return results.map((result) => {
- const node = loomTree.nodeStore[result.id];
- return {
- node: node,
- score: result.score,
- highlightedContent: highlightText(
- result.content || extractPatchContent(node.patch),
- query
- ),
- highlightedSummary: highlightText(result.summary || "", query),
- };
- });
- } catch (error) {
- console.warn("Search error:", error);
- return [];
- }
-}
-
-// Get search suggestions (bonus feature!)
-function getSearchSuggestions(query) {
- if (!searchIndex || !query.trim()) {
- return [];
- }
-
- try {
- return searchIndex.autoSuggest(query, {
- fuzzy: 0.3,
- prefix: true,
- });
- } catch (error) {
- console.warn("Suggestion error:", error);
- return [];
- }
-}
-
-// Simple text highlighting function
-function highlightText(text, query) {
- if (!query.trim() || !text) return text;
-
- const words = query.toLowerCase().split(/\s+/);
- let highlighted = text;
-
- words.forEach((word) => {
- const regex = new RegExp(`(${escapeRegex(word)})`, "gi");
- highlighted = highlighted.replace(regex, "$1 ");
- });
-
- return highlighted;
-}
-
-function escapeRegex(string) {
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-}
-
-// Render search results instead of tree
-function renderSearchResults(query, container) {
- const results = performSearch(query);
- container.innerHTML = "";
-
- if (results.length === 0) {
- const noResults = document.createElement("div");
- noResults.style.cssText = "padding: 10px; color: #666; font-style: italic;";
- noResults.textContent = "No results found";
- container.appendChild(noResults);
- return;
- }
-
- const resultsContainer = document.createElement("div");
- resultsContainer.style.cssText = "max-height: 400px; overflow-y: auto;";
-
- results.forEach((result) => {
- const resultItem = document.createElement("div");
- resultItem.style.cssText = `
- border: 1px solid #ddd;
- margin: 5px 0;
- padding: 8px;
- cursor: pointer;
- border-radius: 3px;
- background: white;
- `;
-
- // Add hover effect
- resultItem.addEventListener("mouseenter", () => {
- resultItem.style.backgroundColor = "#f0f0f0";
- });
- resultItem.addEventListener("mouseleave", () => {
- resultItem.style.backgroundColor = "white";
- });
-
- const header = document.createElement("div");
- header.style.cssText =
- "font-weight: bold; color: #333; margin-bottom: 4px;";
- header.innerHTML = `${result.node.type.toUpperCase()} - ${
- result.highlightedSummary || result.node.summary
- }`;
-
- const content = document.createElement("div");
- content.style.cssText = "font-size: 0.9em; color: #666; line-height: 1.3;";
- const truncatedContent = result.highlightedContent.substring(0, 150);
- content.innerHTML =
- truncatedContent +
- (truncatedContent.length < result.highlightedContent.length ? "..." : "");
-
- const meta = document.createElement("div");
- meta.style.cssText = "font-size: 0.8em; color: #999; margin-top: 4px;";
-
- // Format timestamp in user's timezone
- const formattedDate = new Date(result.node.timestamp).toLocaleString(
- "en-US",
- {
- year: "numeric",
- month: "short",
- day: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- }
- );
-
- meta.innerHTML = `
- Score: ${result.score.toFixed(3)} | ID: ${result.node.id}
- ${formattedDate}
- `;
-
- resultItem.appendChild(header);
- resultItem.appendChild(content);
- resultItem.appendChild(meta);
-
- // Click to focus on node
- resultItem.onclick = () => {
- currentSearchQuery = "";
- searchResultsMode = false;
- document.getElementById("search-input").value = "";
- changeFocus(result.node.id);
- };
-
- resultsContainer.appendChild(resultItem);
- });
-
- container.appendChild(resultsContainer);
-}
-
-// Modified renderTree function to support search mode
-function renderTreeOrSearch(node, container) {
- if (searchResultsMode && currentSearchQuery.trim()) {
- renderSearchResults(currentSearchQuery, container);
- return;
- }
- renderTree(node, container);
-}
-
-// Override the original createNode to update search index
-const originalCreateNode = LoomTree.prototype.createNode;
-LoomTree.prototype.createNode = function (type, parent, text, summary) {
- const node = originalCreateNode.call(this, type, parent, text, summary);
-
- // Add to search index immediately - MiniSearch handles this efficiently!
- if (searchIndex) {
- addNodeToSearchIndex(node);
- }
-
- return node;
-};
-
-// Override the original updateNode to update search index
-const originalUpdateNode = LoomTree.prototype.updateNode;
-LoomTree.prototype.updateNode = function (node, text, summary) {
- originalUpdateNode.call(this, node, text, summary);
-
- // Update in search index - MiniSearch makes this easy
- if (searchIndex) {
- updateNodeInSearchIndex(node);
- }
-};
-
-// Create search bar HTML with suggestions
-function createSearchBar() {
- const searchContainer = document.createElement("div");
- searchContainer.style.cssText = `
- margin-bottom: 10px;
- padding: 0;
- position: relative;
- width: 100%;
- box-sizing: border-box;
- `;
-
- const searchInput = document.createElement("input");
- searchInput.type = "text";
- searchInput.id = "search-input";
- searchInput.placeholder = "Search nodes...";
- searchInput.style.cssText = `
- width: 100%;
- padding: 8px;
- border: 1px solid #ddd;
- border-radius: 3px;
- font-size: 14px;
- box-sizing: border-box;
- `;
-
- // Suggestions dropdown
- const suggestionsContainer = document.createElement("div");
- suggestionsContainer.id = "search-suggestions";
- suggestionsContainer.style.cssText = `
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- background: white;
- border: 1px solid #ddd;
- border-top: none;
- border-radius: 0 0 3px 3px;
- max-height: 150px;
- overflow-y: auto;
- z-index: 1000;
- display: none;
- box-sizing: border-box;
- `;
-
- // Search as you type with debouncing
- let searchTimeout;
- searchInput.addEventListener("input", (e) => {
- clearTimeout(searchTimeout);
- const query = e.target.value;
-
- searchTimeout = setTimeout(() => {
- currentSearchQuery = query;
- searchResultsMode = query.trim().length > 0;
-
- const treeView = document.getElementById("loom-tree-view");
- treeView.innerHTML = "";
-
- if (searchResultsMode) {
- renderSearchResults(query, treeView);
- showSuggestions(query, suggestionsContainer);
- } else {
- renderTree(focus, treeView, 2);
- hideSuggestions(suggestionsContainer);
- }
- }, 200); // 200ms debounce
- });
-
- // Handle suggestion clicks and keyboard navigation
- let suggestionIndex = -1;
- searchInput.addEventListener("keydown", (e) => {
- const suggestions =
- suggestionsContainer.querySelectorAll(".search-suggestion");
-
- if (e.key === "Escape") {
- e.target.value = "";
- currentSearchQuery = "";
- searchResultsMode = false;
- hideSuggestions(suggestionsContainer);
- const treeView = document.getElementById("loom-tree-view");
- treeView.innerHTML = "";
- renderTree(focus, treeView, 2);
- } else if (e.key === "ArrowDown") {
- e.preventDefault();
- suggestionIndex = Math.min(suggestionIndex + 1, suggestions.length - 1);
- updateSuggestionHighlight(suggestions, suggestionIndex);
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- suggestionIndex = Math.max(suggestionIndex - 1, -1);
- updateSuggestionHighlight(suggestions, suggestionIndex);
- } else if (e.key === "Enter" && suggestionIndex >= 0) {
- e.preventDefault();
- const selectedSuggestion = suggestions[suggestionIndex];
- if (selectedSuggestion) {
- searchInput.value = selectedSuggestion.textContent;
- searchInput.dispatchEvent(new Event("input"));
- hideSuggestions(suggestionsContainer);
- }
- }
- });
-
- // Hide suggestions when clicking outside
- document.addEventListener("click", (e) => {
- if (!searchContainer.contains(e.target)) {
- hideSuggestions(suggestionsContainer);
- }
- });
-
- searchContainer.appendChild(searchInput);
- searchContainer.appendChild(suggestionsContainer);
- return searchContainer;
-}
-
-// Show search suggestions
-function showSuggestions(query, container) {
- if (!query.trim()) {
- hideSuggestions(container);
- return;
- }
-
- const suggestions = getSearchSuggestions(query);
- if (suggestions.length === 0) {
- hideSuggestions(container);
- return;
- }
-
- container.innerHTML = "";
- suggestions.slice(0, 5).forEach((suggestion) => {
- // Limit to 5 suggestions
- const item = document.createElement("div");
- item.className = "search-suggestion";
- item.textContent = suggestion.suggestion;
- item.style.cssText = `
- padding: 8px;
- cursor: pointer;
- border-bottom: 1px solid #eee;
- `;
-
- item.addEventListener("mouseenter", () => {
- item.style.backgroundColor = "#f0f0f0";
- });
- item.addEventListener("mouseleave", () => {
- item.style.backgroundColor = "white";
- });
- item.addEventListener("click", () => {
- document.getElementById("search-input").value = suggestion.suggestion;
- document.getElementById("search-input").dispatchEvent(new Event("input"));
- hideSuggestions(container);
- });
-
- container.appendChild(item);
- });
-
- container.style.display = "block";
-}
-
-// Hide suggestions
-function hideSuggestions(container) {
- container.style.display = "none";
-}
-
-// Update suggestion highlight for keyboard navigation
-function updateSuggestionHighlight(suggestions, index) {
- suggestions.forEach((item, i) => {
- if (i === index) {
- item.style.backgroundColor = "#e0e0e0";
- } else {
- item.style.backgroundColor = "white";
- }
- });
-}
-
-// Insert search bar into the DOM
-function insertSearchBar() {
- const loomTreeView = document.getElementById("loom-tree-view");
-
- // Create a wrapper container for the tree area if it doesn't exist
- let treeContainer = document.getElementById("tree-container");
- if (!treeContainer) {
- treeContainer = document.createElement("div");
- treeContainer.id = "tree-container";
- treeContainer.style.cssText = `
- display: flex;
- flex-direction: column;
- width: 300px;
- margin-left: 2em;
- margin-top: 1em;
- `;
-
- // Wrap the existing tree view
- loomTreeView.parentNode.insertBefore(treeContainer, loomTreeView);
- treeContainer.appendChild(loomTreeView);
-
- // Remove the original margin from tree view since container handles it
- loomTreeView.style.marginLeft = "0";
- }
-
- // Insert search bar at the top of the tree container
- const searchBar = createSearchBar();
- treeContainer.insertBefore(searchBar, loomTreeView);
-}
-
-// Initialize the palette dropdown
-function initializePaletteDropdown() {
- const paletteSelect = document.getElementById('paletteSelect');
-
- for (const paletteName in samplerSettingsStore.palettes) {
- const option = document.createElement('option');
- option.value = paletteName;
- option.textContent = paletteName;
- paletteSelect.appendChild(option);
- }
-
- paletteSelect.addEventListener('change', onPaletteChange);
- if ("current-sampler" in samplerSettingsStore) {
- paletteSelect.value = samplerSettingsStore["current-sampler"]["palette"];
- const currentSamplerItem = samplerSettingsStore["current-sampler"]["sampler-item"];
- console.log(currentSamplerItem);
- updateWheel(samplerSettingsStore.palettes[paletteSelect.value]);
- selectSamplerItem(currentSamplerItem);
- }
-}
-
-// Modified renderTick to use new render function
-const originalRenderTick = renderTick;
-window.renderTick = function () {
- // Call most of the original renderTick logic
- editor.value = "";
- var next = focus;
- editor.value = loomTree.renderNode(next);
-
- let parent;
- let selection;
- let batchLimit;
- if (focus.parent) {
- parent = loomTree.nodeStore[focus.parent];
- selection = parent.children.indexOf(focus.id);
- batchLimit = parent.children.length - 1;
- } else {
- selection = 0;
- batchLimit = 0;
- }
-
- const batchIndexMarker = document.getElementById("batch-item-index");
- batchIndexMarker.textContent = `${selection + 1}/${batchLimit + 1}`;
-
- const controls = document.getElementById("controls");
-
- const oldBranchControlsDiv = document.getElementById(
- "prompt-branch-controls"
- );
- if (oldBranchControlsDiv) {
- oldBranchControlsDiv.innerHTML = "";
- oldBranchControlsDiv.remove();
- }
-
- const branchControlsDiv = document.createElement("div");
- branchControlsDiv.id = "prompt-branch-controls";
- branchControlsDiv.classList.add("branch-controls");
-
- const branchControlButtonsDiv = document.createElement("div");
- branchControlButtonsDiv.classList.add("branch-control-buttons");
-
- var leftThumbClass = "thumbs";
- var rightThumbClass = "thumbs";
- if (focus.rating) {
- leftThumbClass = "chosen";
- } else if (focus.rating == false) {
- rightThumbClass = "chosen";
- }
-
- const leftThumbSpan = document.createElement("span");
- leftThumbSpan.classList.add(leftThumbClass);
- leftThumbSpan.textContent = "š";
- leftThumbSpan.onclick = () => promptThumbsUp(focus.id);
-
- const rightThumbSpan = document.createElement("span");
- rightThumbSpan.classList.add(rightThumbClass);
- rightThumbSpan.textContent = "š";
- rightThumbSpan.onclick = () => promptThumbsDown(focus.id);
-
- branchControlButtonsDiv.append(leftThumbSpan, rightThumbSpan);
-
- if (focus.type === "gen") {
- const rewriteButton = document.createElement("span");
- rewriteButton.id = "rewrite-button";
- rewriteButton.textContent = "š¬";
- rewriteButton.onclick = () => promptRewriteNode(focus.id);
-
- // branchControlButtonsDiv.append(rewriteButton);
- }
-
- const genControls = document.createElement("div");
- genControls.id = "gen-controls";
-
- // Build "Sampler Palette Wheel" UI
- const samplerPaletteContainer = document.createElement("div");
- samplerPaletteContainer.classList.add("palette-wheel-widget");
-
- // Palette selector
- const paletteSelector = document.createElement("div");
- paletteSelector.classList.add("palette-selector");
-
- const paletteSelect = document.createElement("select");
- paletteSelect.id = "paletteSelect";
- paletteSelect.classList.add("palette-dropdown");
- const paletteDefaultOpt = document.createElement("option");
- paletteDefaultOpt.value = "";
- paletteDefaultOpt.text = "Choose a palette...";
- paletteSelect.append(paletteDefaultOpt);
-
- paletteSelector.append(paletteSelect);
- samplerPaletteContainer.append(paletteSelector);
-
- const wheelWrapper = document.createElement("div");
- wheelWrapper.classList.add("wheel-wrapper");
-
- // Wheel container
- const wheelContainer = document.createElement("div");
- wheelContainer.id = "wheelContainer";
- wheelContainer.classList.add("wheel-container");
-
- const paletteWheel = document.createElement("div");
- paletteWheel.classList.add("palette-wheel");
- paletteWheel.id = "paletteWheel";
-
- const emptyWheel = document.createElement("div");
- emptyWheel.classList.add("empty-wheel");
- emptyWheel.textContent = "Select a palette to begin";
- paletteWheel.append(emptyWheel);
-
- const wheelCenter = document.createElement("div");
- wheelCenter.id = "wheelCenter";
- wheelCenter.classList.add("wheel-center");
-
- wheelContainer.append(paletteWheel);
- wheelContainer.append(wheelCenter);
- wheelWrapper.append(wheelContainer)
- samplerPaletteContainer.append(wheelWrapper);
-
- // Hidden dropdown controlling selected sampler item
- const samplerItemSelect = document.createElement("select");
- samplerItemSelect.classList.add("hidden-sampler-select");
- samplerItemSelect.id = "samplerItemSelect";
- // If you want it truly hidden without CSS:
- samplerItemSelect.hidden = true; // or: samplerItemSelect.style.display = "none";
- samplerPaletteContainer.append(samplerItemSelect);
-
- // Current selection
- const currentSelection = document.createElement("div");
- currentSelection.classList.add("current-selection");
- currentSelection.classList.add("reroll");
- currentSelection.id = "currentSampler";
- currentSelection.textContent = "None selected";
- currentSelection.onclick = () => reroll(focus.id, false);
- samplerPaletteContainer.append(currentSelection);
-
- genControls.append(samplerPaletteContainer);
- branchControlButtonsDiv.append(genControls);
-
-
- if (focus.type === "weave") {
- const branchScoreSpan = document.createElement("span");
- branchScoreSpan.classList.add("reward-score");
- try {
- const score = focus["nodes"].at(-1).score;
- const prob = 1 / (Math.exp(-score) + 1);
- branchScoreSpan.textContent = (prob * 100).toPrecision(4) + "%";
- } catch (error) {
- branchScoreSpan.textContent = "N.A.";
- }
- branchControlsDiv.append(branchControlButtonsDiv, branchScoreSpan);
- } else {
- branchControlsDiv.append(branchControlButtonsDiv);
- }
-
- controls.append(branchControlsDiv);
-
- focus.read = true;
- const loomTreeView = document.getElementById("loom-tree-view");
- loomTreeView.innerHTML = "";
- renderTreeOrSearch(focus, loomTreeView); // Use our new function
- initializePaletteDropdown();
- errorMessage.textContent = "";
- updateCounterDisplay(editor.value);
-};
-
-// Modified loadFile function to rebuild search index
-const originalLoadFile = loadFile;
-window.loadFile = async function () {
- await originalLoadFile();
- initializeSearchIndex();
-};
-
-// Initialize everything when the page loads
-document.addEventListener("DOMContentLoaded", function () {
- // Insert search bar
- insertSearchBar();
-
- // Initialize search index
- initializeSearchIndex();
-
- console.log("MiniLoom MiniSearch integration loaded");
-});
-
-// Export functions for debugging
-window.searchFunctions = {
- performSearch,
- getSearchSuggestions,
- addNodeToSearchIndex,
- removeNodeFromSearchIndex,
- updateNodeInSearchIndex,
- extractPatchContent,
-};
diff --git a/package-lock.json b/package-lock.json
index afd17de..66fb0c7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,13 +7,61 @@
"": {
"name": "miniloom",
"version": "1.0.0",
- "license": "ISC",
+ "license": "Apache-2.0",
"dependencies": {
"diff-match-patch": "^1.0.5",
"minisearch": "^7.1.2"
},
"devDependencies": {
- "electron": "^37.2.4"
+ "electron": "^37.2.4",
+ "electron-builder": "^25.0.5",
+ "prettier": "^3.6.2"
+ }
+ },
+ "node_modules/@develar/schema-utils": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
+ "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.0",
+ "ajv-keywords": "^3.4.1"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/@electron/asar": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
+ "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
+ "dev": true,
+ "dependencies": {
+ "commander": "^5.0.0",
+ "glob": "^7.1.6",
+ "minimatch": "^3.0.4"
+ },
+ "bin": {
+ "asar": "bin/asar.js"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/@electron/asar/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
}
},
"node_modules/@electron/get": {
@@ -37,400 +85,490 @@
"global-agent": "^3.0.0"
}
},
- "node_modules/@sindresorhus/is": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
- "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "node_modules/@electron/notarize": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
+ "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==",
"dev": true,
- "engines": {
- "node": ">=10"
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.1",
+ "promise-retry": "^2.0.1"
},
- "funding": {
- "url": "https://github.com/sindresorhus/is?sponsor=1"
+ "engines": {
+ "node": ">= 10.0.0"
}
},
- "node_modules/@szmarczak/http-timer": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
- "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "node_modules/@electron/notarize/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dev": true,
"dependencies": {
- "defer-to-connect": "^2.0.0"
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
- "node_modules/@types/cacheable-request": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
- "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "node_modules/@electron/notarize/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"dependencies": {
- "@types/http-cache-semantics": "*",
- "@types/keyv": "^3.1.4",
- "@types/node": "*",
- "@types/responselike": "^1.0.0"
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
}
},
- "node_modules/@types/http-cache-semantics": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
- "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
- "dev": true
- },
- "node_modules/@types/keyv": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
- "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+ "node_modules/@electron/notarize/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
- "dependencies": {
- "@types/node": "*"
+ "engines": {
+ "node": ">= 10.0.0"
}
},
- "node_modules/@types/node": {
- "version": "22.16.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
- "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
+ "node_modules/@electron/osx-sign": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz",
+ "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==",
"dev": true,
"dependencies": {
- "undici-types": "~6.21.0"
+ "compare-version": "^0.1.2",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.0.0",
+ "isbinaryfile": "^4.0.8",
+ "minimist": "^1.2.6",
+ "plist": "^3.0.5"
+ },
+ "bin": {
+ "electron-osx-flat": "bin/electron-osx-flat.js",
+ "electron-osx-sign": "bin/electron-osx-sign.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
}
},
- "node_modules/@types/responselike": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
- "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "node_modules/@electron/osx-sign/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"dependencies": {
- "@types/node": "*"
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/@types/yauzl": {
- "version": "2.10.3",
- "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
- "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "node_modules/@electron/osx-sign/node_modules/isbinaryfile": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
+ "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
"dev": true,
- "optional": true,
- "dependencies": {
- "@types/node": "*"
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
}
},
- "node_modules/boolean": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
- "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
- "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
- "dev": true,
- "optional": true
- },
- "node_modules/buffer-crc32": {
- "version": "0.2.13",
- "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
- "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "node_modules/@electron/osx-sign/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
- "engines": {
- "node": "*"
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
}
},
- "node_modules/cacheable-lookup": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
- "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "node_modules/@electron/osx-sign/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
- "node": ">=10.6.0"
+ "node": ">= 10.0.0"
}
},
- "node_modules/cacheable-request": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
- "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
+ "node_modules/@electron/rebuild": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz",
+ "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==",
"dev": true,
"dependencies": {
- "clone-response": "^1.0.2",
- "get-stream": "^5.1.0",
- "http-cache-semantics": "^4.0.0",
- "keyv": "^4.0.0",
- "lowercase-keys": "^2.0.0",
- "normalize-url": "^6.0.1",
- "responselike": "^2.0.0"
+ "@malept/cross-spawn-promise": "^2.0.0",
+ "chalk": "^4.0.0",
+ "debug": "^4.1.1",
+ "detect-libc": "^2.0.1",
+ "fs-extra": "^10.0.0",
+ "got": "^11.7.0",
+ "node-abi": "^3.45.0",
+ "node-api-version": "^0.2.0",
+ "node-gyp": "^9.0.0",
+ "ora": "^5.1.0",
+ "read-binary-file-arch": "^1.0.6",
+ "semver": "^7.3.5",
+ "tar": "^6.0.5",
+ "yargs": "^17.0.1"
+ },
+ "bin": {
+ "electron-rebuild": "lib/cli.js"
},
"engines": {
- "node": ">=8"
+ "node": ">=12.13.0"
}
},
- "node_modules/clone-response": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
- "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
+ "node_modules/@electron/rebuild/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"dependencies": {
- "mimic-response": "^1.0.0"
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "node_modules/@electron/rebuild/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
+ "universalify": "^2.0.0"
},
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
}
},
- "node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "node_modules/@electron/rebuild/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
- "dependencies": {
- "mimic-response": "^3.1.0"
+ "bin": {
+ "semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/decompress-response/node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "node_modules/@electron/rebuild/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">= 10.0.0"
}
},
- "node_modules/defer-to-connect": {
+ "node_modules/@electron/universal": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
- "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
+ "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==",
"dev": true,
+ "dependencies": {
+ "@electron/asar": "^3.2.7",
+ "@malept/cross-spawn-promise": "^2.0.0",
+ "debug": "^4.3.1",
+ "dir-compare": "^4.2.0",
+ "fs-extra": "^11.1.1",
+ "minimatch": "^9.0.3",
+ "plist": "^3.1.0"
+ },
"engines": {
- "node": ">=10"
+ "node": ">=16.4"
}
},
- "node_modules/define-data-property": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
- "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "node_modules/@electron/universal/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
- "optional": true,
"dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "gopd": "^1.0.1"
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/fs-extra": {
+ "version": "11.3.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
+ "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
}
},
- "node_modules/define-properties": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
- "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "node_modules/@electron/universal/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
- "optional": true,
"dependencies": {
- "define-data-property": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "object-keys": "^1.1.1"
+ "brace-expansion": "^2.0.1"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=16 || 14 >=14.17"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/detect-node": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
- "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "node_modules/@electron/universal/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
- "optional": true
+ "engines": {
+ "node": ">= 10.0.0"
+ }
},
- "node_modules/diff-match-patch": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
- "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
+ "node_modules/@gar/promisify": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
+ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
+ "dev": true
},
- "node_modules/electron": {
- "version": "37.2.4",
- "resolved": "https://registry.npmjs.org/electron/-/electron-37.2.4.tgz",
- "integrity": "sha512-F1WDDvY60TpFwGyW+evNB5q0Em8PamcDTVIKB2NaiaKEbNC2Fabn8Wyxy5g+Anirr1K40eKGjfSJhWEUbI1TOw==",
+ "node_modules/@isaacs/balanced-match": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
+ "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
+ "dev": true,
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@isaacs/brace-expansion": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
+ "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
- "hasInstallScript": true,
"dependencies": {
- "@electron/get": "^2.0.0",
- "@types/node": "^22.7.7",
- "extract-zip": "^2.0.1"
- },
- "bin": {
- "electron": "cli.js"
+ "@isaacs/balanced-match": "^4.0.1"
},
"engines": {
- "node": ">= 12.20.55"
+ "node": "20 || >=22"
}
},
- "node_modules/end-of-stream": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
- "once": "^1.4.0"
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/env-paths": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
- "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
+ "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"engines": {
- "node": ">=6"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
- "optional": true,
"engines": {
- "node": ">= 0.4"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
- "optional": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
"engines": {
- "node": ">= 0.4"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/es6-error": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
- "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
- "optional": true
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
},
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
- "optional": true,
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
"engines": {
- "node": ">=10"
+ "node": ">=12"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/extract-zip": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
- "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "node_modules/@malept/cross-spawn-promise": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
+ "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==",
"dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/malept"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
+ }
+ ],
"dependencies": {
- "debug": "^4.1.1",
- "get-stream": "^5.1.0",
- "yauzl": "^2.10.0"
- },
- "bin": {
- "extract-zip": "cli.js"
+ "cross-spawn": "^7.0.1"
},
"engines": {
- "node": ">= 10.17.0"
- },
- "optionalDependencies": {
- "@types/yauzl": "^2.9.1"
+ "node": ">= 12.13.0"
}
},
- "node_modules/fd-slicer": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
- "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "node_modules/@malept/flatpak-bundler": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz",
+ "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==",
"dev": true,
"dependencies": {
- "pend": "~1.2.0"
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.0",
+ "lodash": "^4.17.15",
+ "tmp-promise": "^3.0.2"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
}
},
- "node_modules/fs-extra": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
- "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dev": true,
"dependencies": {
+ "at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
- "jsonfile": "^4.0.0",
- "universalify": "^0.1.0"
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
},
"engines": {
- "node": ">=6 <7 || >=8"
+ "node": ">=10"
}
},
- "node_modules/get-stream": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
- "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"dependencies": {
- "pump": "^3.0.0"
+ "universalify": "^2.0.0"
},
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
"engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">= 10.0.0"
}
},
- "node_modules/global-agent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
- "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
+ "node_modules/@npmcli/fs": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz",
+ "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==",
"dev": true,
- "optional": true,
"dependencies": {
- "boolean": "^3.0.1",
- "es6-error": "^4.1.1",
- "matcher": "^3.0.0",
- "roarr": "^2.15.3",
- "semver": "^7.3.2",
- "serialize-error": "^7.0.1"
+ "@gar/promisify": "^1.1.3",
+ "semver": "^7.3.5"
},
"engines": {
- "node": ">=10.0"
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
- "node_modules/global-agent/node_modules/semver": {
+ "node_modules/@npmcli/fs/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
- "optional": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -438,242 +576,3971 @@
"node": ">=10"
}
},
- "node_modules/globalthis": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
- "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "node_modules/@npmcli/move-file": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz",
+ "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==",
+ "deprecated": "This functionality has been moved to @npmcli/fs",
"dev": true,
- "optional": true,
"dependencies": {
- "define-properties": "^1.2.1",
- "gopd": "^1.0.1"
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
- "node": ">= 0.4"
+ "node": ">=14"
+ }
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
- "node_modules/got": {
- "version": "11.8.6",
- "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
- "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
+ "node_modules/@szmarczak/http-timer": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"dependencies": {
- "@sindresorhus/is": "^4.0.0",
- "@szmarczak/http-timer": "^4.0.5",
- "@types/cacheable-request": "^6.0.1",
- "@types/responselike": "^1.0.0",
- "cacheable-lookup": "^5.0.3",
- "cacheable-request": "^7.0.2",
- "decompress-response": "^6.0.0",
- "http2-wrapper": "^1.0.0-beta.5.2",
- "lowercase-keys": "^2.0.0",
- "p-cancelable": "^2.0.0",
- "responselike": "^2.0.0"
+ "defer-to-connect": "^2.0.0"
},
"engines": {
- "node": ">=10.19.0"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/got?sponsor=1"
+ "node": ">=10"
}
},
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
},
- "node_modules/has-property-descriptors": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
- "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dev": true,
- "optional": true,
"dependencies": {
- "es-define-property": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
}
},
- "node_modules/http-cache-semantics": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
- "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
- "dev": true
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/ms": "*"
+ }
},
- "node_modules/http2-wrapper": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
- "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "node_modules/@types/fs-extra": {
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
+ "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
"dev": true,
"dependencies": {
- "quick-lru": "^5.1.1",
- "resolve-alpn": "^1.0.0"
- },
- "engines": {
- "node": ">=10.19.0"
+ "@types/node": "*"
}
},
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
+ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"dev": true
},
- "node_modules/json-stringify-safe": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
- "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dev": true,
- "optional": true
+ "dependencies": {
+ "@types/node": "*"
+ }
},
- "node_modules/jsonfile": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
- "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "22.16.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
+ "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
"dev": true,
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
+ "dependencies": {
+ "undici-types": "~6.21.0"
}
},
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "node_modules/@types/plist": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
+ "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
"dev": true,
+ "optional": true,
"dependencies": {
- "json-buffer": "3.0.1"
+ "@types/node": "*",
+ "xmlbuilder": ">=11.0.1"
}
},
- "node_modules/lowercase-keys": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/verror": {
+ "version": "1.10.11",
+ "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
+ "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.11",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+ "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/7zip-bin": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
+ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
+ "dev": true
+ },
+ "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
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "dev": true,
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true,
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/app-builder-bin": {
+ "version": "5.0.0-alpha.10",
+ "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz",
+ "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==",
+ "dev": true
+ },
+ "node_modules/app-builder-lib": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz",
+ "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==",
+ "dev": true,
+ "dependencies": {
+ "@develar/schema-utils": "~2.6.5",
+ "@electron/notarize": "2.5.0",
+ "@electron/osx-sign": "1.3.1",
+ "@electron/rebuild": "3.6.1",
+ "@electron/universal": "2.0.1",
+ "@malept/flatpak-bundler": "^0.4.0",
+ "@types/fs-extra": "9.0.13",
+ "async-exit-hook": "^2.0.1",
+ "bluebird-lst": "^1.0.9",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chromium-pickle-js": "^0.2.0",
+ "config-file-ts": "0.2.8-rc1",
+ "debug": "^4.3.4",
+ "dotenv": "^16.4.5",
+ "dotenv-expand": "^11.0.6",
+ "ejs": "^3.1.8",
+ "electron-publish": "25.1.7",
+ "form-data": "^4.0.0",
+ "fs-extra": "^10.1.0",
+ "hosted-git-info": "^4.1.0",
+ "is-ci": "^3.0.0",
+ "isbinaryfile": "^5.0.0",
+ "js-yaml": "^4.1.0",
+ "json5": "^2.2.3",
+ "lazy-val": "^1.0.5",
+ "minimatch": "^10.0.0",
+ "resedit": "^1.7.0",
+ "sanitize-filename": "^1.6.3",
+ "semver": "^7.3.8",
+ "tar": "^6.1.12",
+ "temp-file": "^3.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "dmg-builder": "25.1.8",
+ "electron-builder-squirrel-windows": "25.1.8"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/aproba": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
+ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
+ "dev": true
+ },
+ "node_modules/archiver": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
+ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "async": "^3.2.4",
+ "buffer-crc32": "^0.2.1",
+ "readable-stream": "^3.6.0",
+ "readdir-glob": "^1.1.2",
+ "tar-stream": "^2.2.0",
+ "zip-stream": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/archiver-utils/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+ "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true
+ },
+ "node_modules/async-exit-hook": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
+ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true
+ },
+ "node_modules/bluebird-lst": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz",
+ "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==",
+ "dev": true,
+ "dependencies": {
+ "bluebird": "^3.5.5"
+ }
+ },
+ "node_modules/boolean": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "node_modules/builder-util": {
+ "version": "25.1.7",
+ "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz",
+ "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==",
+ "dev": true,
+ "dependencies": {
+ "@types/debug": "^4.1.6",
+ "7zip-bin": "~5.2.0",
+ "app-builder-bin": "5.0.0-alpha.10",
+ "bluebird-lst": "^1.0.9",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "cross-spawn": "^7.0.3",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.0",
+ "is-ci": "^3.0.0",
+ "js-yaml": "^4.1.0",
+ "source-map-support": "^0.5.19",
+ "stat-mode": "^1.0.0",
+ "temp-file": "^3.4.0"
+ }
+ },
+ "node_modules/builder-util-runtime": {
+ "version": "9.2.10",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz",
+ "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4",
+ "sax": "^1.2.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/builder-util/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/builder-util/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/builder-util/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "16.1.3",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz",
+ "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==",
+ "dev": true,
+ "dependencies": {
+ "@npmcli/fs": "^2.1.0",
+ "@npmcli/move-file": "^2.0.0",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.1.0",
+ "glob": "^8.0.1",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^7.7.1",
+ "minipass": "^3.1.6",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "mkdirp": "^1.0.4",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^9.0.0",
+ "tar": "^6.1.11",
+ "unique-filename": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cacache/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
+ "dev": true,
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^4.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chromium-pickle-js": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
+ "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==",
+ "dev": true
+ },
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
+ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
+ "dev": true,
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "dev": true,
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+ "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/compare-version": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
+ "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/compress-commons": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
+ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "buffer-crc32": "^0.2.13",
+ "crc32-stream": "^4.0.2",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/config-file-ts": {
+ "version": "0.2.8-rc1",
+ "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz",
+ "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^10.3.12",
+ "typescript": "^5.4.3"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "dev": true
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
+ "dev": true
+ },
+ "node_modules/crc": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
+ "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "buffer": "^5.1.0"
+ }
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
+ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^3.4.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dev": true,
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "dev": true
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/diff-match-patch": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
+ "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
+ },
+ "node_modules/dir-compare": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
+ "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==",
+ "dev": true,
+ "dependencies": {
+ "minimatch": "^3.0.5",
+ "p-limit": "^3.1.0 "
+ }
+ },
+ "node_modules/dir-compare/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/dmg-builder": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz",
+ "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==",
+ "dev": true,
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "fs-extra": "^10.1.0",
+ "iconv-lite": "^0.6.2",
+ "js-yaml": "^4.1.0"
+ },
+ "optionalDependencies": {
+ "dmg-license": "^1.0.11"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/dmg-license": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
+ "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "@types/plist": "^3.0.1",
+ "@types/verror": "^1.10.3",
+ "ajv": "^6.10.0",
+ "crc": "^3.8.0",
+ "iconv-corefoundation": "^1.1.7",
+ "plist": "^3.0.4",
+ "smart-buffer": "^4.0.2",
+ "verror": "^1.10.0"
+ },
+ "bin": {
+ "dmg-license": "bin/dmg-license.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dotenv-expand": {
+ "version": "11.0.7",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
+ "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==",
+ "dev": true,
+ "dependencies": {
+ "dotenv": "^16.4.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "dev": true,
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/electron": {
+ "version": "37.2.4",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-37.2.4.tgz",
+ "integrity": "sha512-F1WDDvY60TpFwGyW+evNB5q0Em8PamcDTVIKB2NaiaKEbNC2Fabn8Wyxy5g+Anirr1K40eKGjfSJhWEUbI1TOw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@electron/get": "^2.0.0",
+ "@types/node": "^22.7.7",
+ "extract-zip": "^2.0.1"
+ },
+ "bin": {
+ "electron": "cli.js"
+ },
+ "engines": {
+ "node": ">= 12.20.55"
+ }
+ },
+ "node_modules/electron-builder": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz",
+ "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==",
+ "dev": true,
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "dmg-builder": "25.1.8",
+ "fs-extra": "^10.1.0",
+ "is-ci": "^3.0.0",
+ "lazy-val": "^1.0.5",
+ "simple-update-notifier": "2.0.0",
+ "yargs": "^17.6.2"
+ },
+ "bin": {
+ "electron-builder": "cli.js",
+ "install-app-deps": "install-app-deps.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz",
+ "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "archiver": "^5.3.1",
+ "builder-util": "25.1.7",
+ "fs-extra": "^10.1.0"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-builder/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-builder/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-builder/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-publish": {
+ "version": "25.1.7",
+ "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz",
+ "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==",
+ "dev": true,
+ "dependencies": {
+ "@types/fs-extra": "^9.0.11",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "fs-extra": "^10.1.0",
+ "lazy-val": "^1.0.5",
+ "mime": "^2.5.2"
+ }
+ },
+ "node_modules/electron-publish/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-publish/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-publish/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es6-error": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/exponential-backoff": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
+ "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==",
+ "dev": true
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extsprintf": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
+ "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "optional": true
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
+ "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+ "dev": true,
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gauge": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+ "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.3",
+ "console-control-strings": "^1.1.0",
+ "has-unicode": "^2.0.1",
+ "signal-exit": "^3.0.7",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/global-agent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "es6-error": "^4.1.1",
+ "matcher": "^3.0.0",
+ "roarr": "^2.15.3",
+ "semver": "^7.3.2",
+ "serialize-error": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10.0"
+ }
+ },
+ "node_modules/global-agent/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/got": {
+ "version": "11.8.6",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
+ "dev": true,
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "dev": true
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "dev": true
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http2-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "dev": true,
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/iconv-corefoundation": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
+ "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "cli-truncate": "^2.1.0",
+ "node-addon-api": "^1.6.3"
+ },
+ "engines": {
+ "node": "^8.11.2 || >=10"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/infer-owner": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
+ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
+ "dev": true
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "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.",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/ip-address": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
+ "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/is-ci": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+ "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+ "dev": true,
+ "dependencies": {
+ "ci-info": "^3.2.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-lambda": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+ "dev": true
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/isbinaryfile": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz",
+ "integrity": "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "dev": true,
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/lazy-val": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
+ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
+ "dev": true
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/lodash.difference": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/lodash.union": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
- "node_modules/matcher": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
- "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
+ "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==",
+ "dev": true,
+ "dependencies": {
+ "agentkeepalive": "^4.2.1",
+ "cacache": "^16.1.0",
+ "http-cache-semantics": "^4.1.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-lambda": "^1.0.1",
+ "lru-cache": "^7.7.1",
+ "minipass": "^3.1.6",
+ "minipass-collect": "^1.0.2",
+ "minipass-fetch": "^2.0.3",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.3",
+ "promise-retry": "^2.0.1",
+ "socks-proxy-agent": "^7.0.0",
+ "ssri": "^9.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/matcher": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "escape-string-regexp": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
+ "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz",
+ "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.1.6",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.1.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minisearch": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz",
+ "integrity": "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA=="
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.77.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz",
+ "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
+ "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/node-api-version": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",
+ "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.3.5"
+ }
+ },
+ "node_modules/node-api-version/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz",
+ "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==",
+ "dev": true,
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^10.0.3",
+ "nopt": "^6.0.0",
+ "npmlog": "^6.0.0",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^12.13 || ^14.13 || >=16"
+ }
+ },
+ "node_modules/node-gyp/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
+ "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "^1.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+ "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "dependencies": {
+ "are-we-there-yet": "^3.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^4.0.3",
+ "set-blocking": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "dev": true,
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-cancelable": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "node_modules/path-scurry/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/pe-library": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz",
+ "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jet2jet"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/plist": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
+ "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
+ "dev": true,
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.8",
+ "base64-js": "^1.5.1",
+ "xmlbuilder": "^15.1.1"
+ },
+ "engines": {
+ "node": ">=10.4.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "dev": true
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-binary-file-arch": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
+ "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "bin": {
+ "read-binary-file-arch": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdir-glob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "minimatch": "^5.1.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resedit": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz",
+ "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==",
+ "dev": true,
+ "dependencies": {
+ "pe-library": "^0.4.1"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jet2jet"
+ }
+ },
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "dev": true
+ },
+ "node_modules/responselike": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
+ "dev": true,
+ "dependencies": {
+ "lowercase-keys": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/roarr": {
+ "version": "2.15.4",
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "detect-node": "^2.0.4",
+ "globalthis": "^1.0.1",
+ "json-stringify-safe": "^5.0.1",
+ "semver-compare": "^1.0.0",
+ "sprintf-js": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/sanitize-filename": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
+ "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+ "dev": true,
+ "dependencies": {
+ "truncate-utf8-bytes": "^1.0.0"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+ "dev": true
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/serialize-error": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "type-fest": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "dev": true
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "dev": true,
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
+ "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.3",
+ "socks": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/ssri": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz",
+ "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==",
+ "dev": true,
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/stat-mode": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
+ "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sumchecker": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
+ "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"dev": true,
- "optional": true,
"dependencies": {
- "escape-string-regexp": "^4.0.0"
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
- "node_modules/mimic-response": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
- "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
+ "peer": true,
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
"engines": {
- "node": ">=4"
+ "node": ">=6"
}
},
- "node_modules/minisearch": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz",
- "integrity": "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA=="
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true
- },
- "node_modules/normalize-url": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
- "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"dev": true,
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=8"
}
},
- "node_modules/object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "node_modules/temp-file": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
+ "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==",
"dev": true,
- "optional": true,
+ "dependencies": {
+ "async-exit-hook": "^2.0.1",
+ "fs-extra": "^10.0.0"
+ }
+ },
+ "node_modules/temp-file/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
"engines": {
- "node": ">= 0.4"
+ "node": ">=12"
}
},
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "node_modules/temp-file/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"dependencies": {
- "wrappy": "1"
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
}
},
- "node_modules/p-cancelable": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
- "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+ "node_modules/temp-file/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
- "node": ">=8"
+ "node": ">= 10.0.0"
}
},
- "node_modules/pend": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
- "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
- "dev": true
- },
- "node_modules/progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"engines": {
- "node": ">=0.4.0"
+ "node": ">=14.14"
}
},
- "node_modules/pump": {
+ "node_modules/tmp-promise": {
"version": "3.0.3",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
- "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
+ "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
"dev": true,
"dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
+ "tmp": "^0.2.0"
}
},
- "node_modules/quick-lru": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
- "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "node_modules/truncate-utf8-bytes": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+ "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+ "dev": true,
+ "dependencies": {
+ "utf8-byte-length": "^1.0.1"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
+ "optional": true,
"engines": {
"node": ">=10"
},
@@ -681,119 +4548,160 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/resolve-alpn": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
- "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
},
- "node_modules/responselike": {
+ "node_modules/unique-filename": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
- "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
+ "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==",
"dev": true,
"dependencies": {
- "lowercase-keys": "^2.0.0"
+ "unique-slug": "^3.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
- "node_modules/roarr": {
- "version": "2.15.4",
- "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
- "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
+ "node_modules/unique-slug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz",
+ "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==",
"dev": true,
- "optional": true,
"dependencies": {
- "boolean": "^3.0.1",
- "detect-node": "^2.0.4",
- "globalthis": "^1.0.1",
- "json-stringify-safe": "^5.0.1",
- "semver-compare": "^1.0.0",
- "sprintf-js": "^1.1.2"
+ "imurmurhash": "^0.1.4"
},
"engines": {
- "node": ">=8.0"
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
- "node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
- "bin": {
- "semver": "bin/semver.js"
+ "engines": {
+ "node": ">= 4.0.0"
}
},
- "node_modules/semver-compare": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
- "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
- "optional": true
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
},
- "node_modules/serialize-error": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
- "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
+ "node_modules/utf8-byte-length": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
+ "dev": true
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "node_modules/verror": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
+ "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
"dev": true,
"optional": true,
"dependencies": {
- "type-fest": "^0.13.1"
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
},
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=0.6.0"
}
},
- "node_modules/sprintf-js": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
- "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
"dev": true,
- "optional": true
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
},
- "node_modules/sumchecker": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
- "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
- "debug": "^4.1.0"
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
},
"engines": {
- "node": ">= 8.0"
+ "node": ">= 8"
}
},
- "node_modules/type-fest": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
- "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dev": true,
- "optional": true,
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
"engines": {
"node": ">=10"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/undici-types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true
- },
- "node_modules/universalify": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
- "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
"engines": {
- "node": ">= 4.0.0"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
@@ -802,6 +4710,57 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
+ "node_modules/xmlbuilder": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
+ "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
@@ -811,6 +4770,55 @@
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zip-stream": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
+ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "archiver-utils": "^3.0.4",
+ "compress-commons": "^4.1.2",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/zip-stream/node_modules/archiver-utils": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
+ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "glob": "^7.2.3",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
}
}
}
diff --git a/package.json b/package.json
index 264c9b6..cc025c0 100644
--- a/package.json
+++ b/package.json
@@ -3,15 +3,112 @@
"productName": "MiniHF MiniLoom",
"version": "1.0.0",
"description": "MiniHF MiniLoom - AI Text Generation Tool",
- "main": "main.js",
+ "main": "src/main.js",
+ "homepage": "https://github.com/JD-P/miniloom",
"scripts": {
- "start": "electron ."
+ "start": "electron .",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "build": "npm run format:check && electron-builder",
+ "build:win": "npm run format:check && electron-builder --win",
+ "build:mac": "npm run format:check && electron-builder --mac",
+ "build:linux": "npm run format:check && electron-builder --linux",
+ "dist": "npm run build",
+ "pack": "electron-builder --dir"
+ },
+ "build": {
+ "appId": "com.minihf.miniloom",
+ "productName": "MiniHF MiniLoom",
+ "directories": {
+ "output": "dist"
+ },
+ "files": [
+ "src/**/*",
+ "prompts/**/*",
+ "assets/**/*",
+ "node_modules/**/*",
+ "package.json",
+ "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
+ "!**/electron-builder.env"
+ ],
+ "mac": {
+ "category": "public.app-category.productivity",
+ "target": [
+ {
+ "target": "dmg",
+ "arch": [
+ "x64",
+ "arm64"
+ ]
+ },
+ {
+ "target": "zip",
+ "arch": [
+ "x64",
+ "arm64"
+ ]
+ }
+ ],
+ "icon": "assets/icon.icns"
+ },
+ "win": {
+ "target": [
+ {
+ "target": "nsis",
+ "arch": [
+ "x64"
+ ]
+ },
+ {
+ "target": "portable",
+ "arch": [
+ "x64"
+ ]
+ }
+ ],
+ "icon": "assets/icon.ico"
+ },
+ "linux": {
+ "target": [
+ {
+ "target": "AppImage",
+ "arch": [
+ "x64"
+ ]
+ },
+ {
+ "target": "deb",
+ "arch": [
+ "x64"
+ ]
+ }
+ ],
+ "icon": "assets/icon.png",
+ "category": "Office"
+ },
+ "nsis": {
+ "oneClick": false,
+ "perMachine": false,
+ "allowToChangeInstallationDirectory": true,
+ "createDesktopShortcut": true,
+ "createStartMenuShortcut": true
+ },
+ "publish": {
+ "provider": "github",
+ "owner": "JD-P",
+ "repo": "miniloom"
+ }
},
"keywords": [],
- "author": "",
- "license": "ISC",
+ "author": {
+ "name": "mimi10v3",
+ "email": "mimi.grace.brooks@gmail.com"
+ },
+ "license": "Apache-2.0",
"devDependencies": {
- "electron": "^37.2.4"
+ "electron": "^37.2.4",
+ "prettier": "^3.6.2",
+ "electron-builder": "^25.0.5"
},
"dependencies": {
"diff-match-patch": "^1.0.5",
diff --git a/renderer.js b/renderer.js
deleted file mode 100644
index 6d15371..0000000
--- a/renderer.js
+++ /dev/null
@@ -1,1361 +0,0 @@
-const fs = require("fs");
-const path = require("path");
-const { ipcRenderer } = require("electron");
-const DiffMatchPatch = require("diff-match-patch");
-const dmp = new DiffMatchPatch();
-const MiniSearch = require("minisearch");
-
-const settingUseWeave = document.getElementById("use-weave");
-const settingNewTokens = document.getElementById("new-tokens");
-const settingBudget = document.getElementById("budget");
-const context = document.getElementById("context");
-const editor = document.getElementById("editor");
-const promptTokenCounter = document.getElementById("prompt-token-counter");
-const saveBtn = document.getElementById("save");
-const loadBtn = document.getElementById("load");
-const errorMessage = document.getElementById("error-message");
-
-class Node {
- constructor(id, type, parent, patch, summary) {
- this.id = id;
- this.timestamp = Date.now();
- this.type = type;
- this.patch = patch;
- this.summary = summary;
- this.cache = false;
- this.rating = null;
- this.read = false;
- this.parent = parent;
- this.children = [];
- }
-}
-
-class LoomTree {
- constructor() {
- this.root = new Node("1", "root", null, "", "Root Node");
- this.nodeStore = { 1: this.root };
- }
-
- createNode(type, parent, text, summary) {
- const parentRenderedText = this.renderNode(parent);
- const patch = dmp.patch_make(parentRenderedText, text);
- const newNodeId = String(Object.keys(this.nodeStore).length + 1);
- const newNode = new Node(newNodeId, type, parent.id, patch, summary);
- if (newNode.type == "user") {
- newNode.read = true;
- }
- parent.children.push(newNodeId);
- this.nodeStore[newNodeId] = newNode;
- return newNode;
- }
-
- updateNode(node, text, summary) {
- // Update a user written leaf
- if (node.type == "gen") {
- return;
- } else if (node.children.length > 0) {
- return;
- }
- const parent = this.nodeStore[node.parent];
- const parentRenderedText = this.renderNode(parent);
- const patch = dmp.patch_make(parentRenderedText, text);
- node.timestamp = Date.now();
- node.patch = patch;
- node.summary = summary;
- }
-
- renderNode(node) {
- if (node == this.root) {
- return "";
- }
- if (node.cache) {
- return node.cache;
- }
- const patches = [];
- patches.push(node.patch);
- const cacheNode = node;
- while (node.parent !== null) {
- node = this.nodeStore[node.parent];
- patches.push(node.patch);
- }
- patches.reverse();
- var outText = "";
- for (let patch of patches) {
- if (patch == "") {
- continue;
- }
- var [outText, results] = dmp.patch_apply(patch, outText);
- }
- /* Disable cache: Not worth the filesize increase
- if (cacheNode.children.length > 0) {
- cacheNode.cache = outText;
- }
- */
- return outText;
- }
-
- serialize(node = this.root) {
- return JSON.stringify(this._serializeHelper(node), null, 2);
- }
-
- _serializeHelper(node) {
- const serializedChildren = node.children.map((child) =>
- this._serializeHelper(this.nodeStore[child])
- );
- return {
- timestamp: node.timestamp,
- type: node.type,
- patch: node.patch,
- rating: node.rating,
- children: serializedChildren,
- };
- }
-}
-
-const MAX_PARENTS = 2;
-const MAX_CHILDREN = 5;
-
-function renderTree(node, container) {
- let parentIds = [];
- for (let i = 0; i < MAX_PARENTS; i++) {
- if (node.parent === null) {
- break; // Stop at root node
- }
- parentIds.push(node.parent);
- node = loomTree.nodeStore[node.parent];
- }
-
- const ul = document.createElement("ul");
- const li = createTreeLi(node, 1, false, parentIds);
- if (node.parent) {
- li.classList.add("hidden-parents");
- }
- ul.appendChild(li);
- renderChildren(node, li, MAX_CHILDREN, parentIds);
- container.appendChild(ul);
-}
-
-function renderChildren(node, container, maxChildrenDepth, parentIds) {
- if (maxChildrenDepth <= 0) return; // Stop recursion when maxChildrenDepth reaches 0 or there are no children
- if (node.children.length == 0) return;
-
- const childrenUl = document.createElement("ul");
- for (let i = 0; i < node.children.length; i++) {
- const child = loomTree.nodeStore[node.children[i]];
- const li = createTreeLi(child, i + 1, maxChildrenDepth <= 1, parentIds);
- childrenUl.appendChild(li);
- renderChildren(child, li, maxChildrenDepth - 1, parentIds);
- }
- container.appendChild(childrenUl);
-}
-
-function createTreeLi(node, index, isMaxDepth, parentIds) {
- // todo: consider using up/down votes to adjust max depth of children displayed
- // todo: consider adding a die icon for generation in progress / an error icon if it fails
-
- const li = document.createElement("li");
- const nodeSpan = document.createElement("span");
-
- if (node.id == focus.id) {
- nodeSpan.id = "focused-node";
- }
- nodeSpan.classList.add(node.read ? "read-tree-node" : "unread-tree-node");
- nodeSpan.classList.add("type-" + node.type);
-
- if (parentIds && parentIds.includes(node.id)) {
- nodeSpan.classList.add("parent-of-focused");
- }
- if (node.rating === true || node.rating === false) {
- nodeSpan.classList.add(node.rating ? "upvoted" : "downvoted");
- }
- if (isMaxDepth && node.children && node.children.length > 0) {
- nodeSpan.classList.add("hidden-children");
- }
-
- const link = document.createElement("a");
- link.textContent = (node.summary || "").trim() || "Option " + index;
- link.title = index + ". " + link.textContent;
-
- link.onclick = (event) => {
- event.stopPropagation(); // Stop event bubbling
- changeFocus(node.id);
- };
- nodeSpan.append(link);
- li.append(nodeSpan);
- return li;
-}
-
-var loomTree = new LoomTree();
-const loomTreeView = document.getElementById("loom-tree-view");
-let focus = loomTree.nodeStore["1"];
-renderTree(loomTree.root, loomTreeView);
-
-function renderTick() {
- editor.value = "";
- var next = focus;
- editor.value = loomTree.renderNode(next);
-
- let parent;
- let selection;
- let batchLimit;
- if (focus.parent) {
- parent = loomTree.nodeStore[focus.parent];
- selection = parent.children.indexOf(focus.id);
- batchLimit = parent.children.length - 1;
- } else {
- selection = 0;
- batchLimit = 0;
- }
-
- const batchIndexMarker = document.getElementById("batch-item-index");
- batchIndexMarker.textContent = `${selection + 1}/${batchLimit + 1}`;
-
- const controls = document.getElementById("controls");
-
- const oldBranchControlsDiv = document.getElementById(
- "prompt-branch-controls"
- );
- if (oldBranchControlsDiv) {
- oldBranchControlsDiv.innerHTML = "";
- oldBranchControlsDiv.remove();
- }
-
- const branchControlsDiv = document.createElement("div");
- branchControlsDiv.id = "prompt-branch-controls";
- branchControlsDiv.classList.add("branch-controls");
-
- const branchControlButtonsDiv = document.createElement("div");
- branchControlButtonsDiv.classList.add("branch-control-buttons");
-
- var leftThumbClass = "thumbs";
- var rightThumbClass = "thumbs";
- if (focus.rating) {
- leftThumbClass = "chosen";
- } else if (focus.rating == false) {
- rightThumbClass = "chosen";
- }
-
- const leftThumbSpan = document.createElement("span");
- leftThumbSpan.classList.add(leftThumbClass);
- leftThumbSpan.textContent = "š";
- leftThumbSpan.onclick = () => promptThumbsUp(focus.id);
-
- const rightThumbSpan = document.createElement("span");
- rightThumbSpan.classList.add(rightThumbClass);
- rightThumbSpan.textContent = "š";
- rightThumbSpan.onclick = () => promptThumbsDown(focus.id);
-
- branchControlButtonsDiv.append(leftThumbSpan, rightThumbSpan);
-
- // TODO: re-enable or remove this feature
- if (focus.type === "gen") {
- const rewriteButton = document.createElement("span");
- rewriteButton.id = "rewrite-button";
- rewriteButton.textContent = "š¬";
- rewriteButton.onclick = () => promptRewriteNode(focus.id);
- // branchControlButtonsDiv.append(rewriteButton);
- }
- const quickRollSpan = document.createElement("span");
- quickRollSpan.classList.add("reroll");
- quickRollSpan.textContent = "šļø";
- quickRollSpan.onclick = () => reroll(focus.id, false);
- branchControlButtonsDiv.append(quickRollSpan);
-
- const samplerSelect = document.createElement("select");
- const samplerOptionOpenAIComp = document.createElement("option");
- samplerOptionOpenAIComp.value = "openai";
- samplerOptionOpenAIComp.text = "OpenAI Completions";
- const samplerOptionOpenAIChat = document.createElement("option");
- samplerOptionOpenAIChat.value = "openai-chat";
- samplerOptionOpenAIChat.text = "OpenAI Chat Completions";
- const samplerOptionOpenRouter = document.createElement("option")
- samplerOptionOpenRouter.value = "openrouter";
- samplerOptionOpenRouter.text = "OpenRouter API";
- const samplerOptionTogether = document.createElement("option");
- samplerOptionTogether.value = "together";
- samplerOptionTogether.text = "Together API";
- samplerSelect.append(samplerOptionOpenAIComp);
- samplerSelect.append(samplerOptionOpenAIChat);
- samplerSelect.append(samplerOptionOpenRouter);
- samplerSelect.append(samplerOptionTogether);
- branchControlButtonsDiv.append(samplerSelect);
-
- const samplerPresets = document.createElement("select");
- for (let samplerPresetName of Object.keys(samplerSettingsStore["sampler-settings"])) {
- const samplerPresetOption = document.createElement("option");
- samplerPresetOption.value = samplerPresetName;
- samplerPresetOption.text = samplerPresetName;
- samplerPresets.append(samplerPresetOption);
- }
- branchControlButtonsDiv.append(samplerPresets);
-
- console.log(branchControlButtonsDiv);
-
- if (focus.type === "weave") {
- const branchScoreSpan = document.createElement("span");
- branchScoreSpan.classList.add("reward-score");
- try {
- const score = focus["nodes"].at(-1).score;
- const prob = 1 / (Math.exp(-score) + 1);
- branchScoreSpan.textContent = (prob * 100).toPrecision(4) + "%";
- } catch (error) {
- branchScoreSpan.textContent = "N.A.";
- }
- branchControlsDiv.append(branchControlButtonsDiv, branchScoreSpan);
- } else {
- branchControlsDiv.append(branchControlButtonsDiv);
- }
-
- controls.append(branchControlsDiv);
-
- focus.read = true;
- loomTreeView.innerHTML = "";
- renderTree(focus, loomTreeView);
- errorMessage.textContent = "";
- updateCounterDisplay(editor.value);
-}
-
-function rotate(direction) {
- const parent = responseDict[focus.parent];
- const selection = parent.children.indexOf(focus.id);
- if (direction === "left" && selection > 0) {
- focus = responseDict[parent.children[selection - 1]];
- } else if (direction === "right" && selection < parent.children.length - 1) {
- focus = responseDict[parent.children[selection + 1]];
- }
- renderResponses();
-}
-
-function changeFocus(newFocusId) {
- focus = loomTree.nodeStore[newFocusId];
- // Prevent focus change from interrupting users typing
- if (focus.type === "user" && focus.children.length == 0) {
- loomTreeView.innerHTML = "";
- renderTree(focus, loomTreeView);
- } else {
- renderTick();
- editor.selectionStart = editor.value.length;
- editor.selectionEnd = editor.value.length;
- editor.focus();
- }
-}
-
-function prepareRollParams() {
- const paletteName = document.getElementById("paletteSelect").value;
- const paletteSamplerName = document.getElementById("samplerItemSelect").value;
- const paletteSamplerItem = samplerSettingsStore["palettes"][paletteName][paletteSamplerName];
- console.log(paletteSamplerItem);
- const sampler = paletteSamplerItem["samplerType"];
- const presetName = paletteSamplerItem["samplerPreset"];
- const apiKeyName = paletteSamplerItem["apiKey"];
- const preset = samplerSettingsStore["sampler-settings"][presetName];
- const apiKey = samplerSettingsStore["api-keys"][apiKeyName];
- let apiUrl;
- if ("api-url" in preset) {
- apiUrl = preset["api-url"]["value"];
- }
- else {
- apiUrl = "";
- }
- let outputBranches;
- if ("output-branches" in preset) {
- outputBranches = preset["output-branches"]["value"];
- }
- else {
- outputBranches = 2;
- }
- let tokensPerBranch;
- if ("tokens-per-branch" in preset) {
- tokensPerBranch = preset["tokens-per-branch"]["value"];
- }
- else {
- tokensPerBranch = 256;
- }
- let temperature;
- if ("temperature" in preset) {
- temperature = preset["temperature"]["value"];
- }
- else {
- temperature = 0.9;
- }
- let topP;
- if ("top-p" in preset) {
- topP = preset["top-p"]["value"];
- }
- else {
- topP = 1;
- }
- let topK;
- if ("top-k" in preset) {
- topK = preset["top-k"]["value"];
- }
- else {
- topK = 100;
- }
- let repetitionPenalty;
- if ("repetition-penalty" in preset) {
- repetitionPenalty = preset["repetition-penalty"]["value"];
- }
- else {
- repetitionPenalty = 1;
- }
- let apiDelay;
- if ("api-delay" in preset) {
- apiDelay = preset["api-delay"]["value"];
- }
- else {
- apiDelay = 3000;
- }
- let modelName;
- if ("model-name" in preset) {
- modelName = preset["model-name"]["value"];
- }
- else {
- modelName = "";
- }
-
- return {"sampler":sampler,
- "api-url":apiUrl,
- "output-branches":outputBranches,
- "tokens-per-branch":tokensPerBranch,
- "temperature":temperature,
- "top-p":topP,
- "top-k":topK,
- "repetition-penalty":repetitionPenalty,
- "api-delay":apiDelay,
- "model-name":modelName,
- "api-key":apiKey,}
-}
-
-async function getResponses(
- endpoint,
- {
- prompt,
- weave = true,
- weaveParams = {},
- focusId = null,
- includePrompt = false,
- }
-) {
- let wp = weaveParams;
- if (focusId) {
- loomTree.renderNode(loomTree.nodeStore[focusId]);
- }
- if (weave) {
- endpoint = endpoint + "weave";
- } else {
- endpoint = endpoint + "generate";
- }
-
- r = await fetch(endpoint, {
- method: "POST",
- body: JSON.stringify({
- prompt: prompt,
- prompt_node: includePrompt,
- tokens_per_branch: wp["tokens_per_branch"],
- output_branches: wp["output_branches"],
- }),
- headers: {
- "Content-type": "application/json; charset=UTF-8",
- },
- });
- batch = await r.json();
- return batch;
-}
-
-async function getSummary(taskText) {
- const params = prepareRollParams();
- const endpoint = params["api-url"];
- const summarizePromptPath = path.join(__dirname, "prompts", "summarize.txt");
- const summarizePromptTemplate = fs.readFileSync(summarizePromptPath, "utf8");
- const summarizePrompt = summarizePromptTemplate.replace("{MODEL_NAME}", params["model-name"]);
- // Limit context to 8 * 512, where eight is the average number of letters in a word
- // and 512 is the number of words to summarize over
- // otherwise we eventually end up pushing the few shot prompt out of the context window
- const prompt =
- summarizePrompt +
- "\n\n" +
- "\n" +
- taskText.slice(-4096) +
- "\n \n\nThree Words:";
- // TODO: Flip this case around
- if (
- !["together", "openrouter", "openai", "openai-chat"].includes(params["sampler"])
- ) {
- r = await fetch(endpoint + "generate", {
- method: "POST",
- body: JSON.stringify({
- prompt: prompt,
- prompt_node: true,
- evaluationPrompt: "",
- tokens_per_branch: 10,
- output_branches: 1,
- }),
- headers: {
- "Content-type": "application/json; charset=UTF-8",
- },
- });
- let batch = await r.json();
- // Always get last three words
- return batch[1]["text"].trim().split("\n")[0].split(" ").slice(0,3).join(" ");
- } // TODO: Figure out how I might have to change this if I end up supporting
- // multiple APIs
- else if (params["sampler"] == "openai-chat") {
- r = await fetch(endpoint, {
- method: "POST",
- body: JSON.stringify({
- messages: [{ role: "system", content: prompt }],
- model: params["model-name"],
- max_tokens: 10,
- temperature: params["temperature"],
- top_p: params["top-p"],
- top_k: params["top-k"],
- repetition_penalty: params["repetition-penalty"],
- }),
- headers: {
- "Content-type": "application/json; charset=UTF-8",
- },
- });
- let batch = await r.json();
- return batch.choices[0]["message"]["content"].trim().split("\n")[0].split(" ").slice(0,3).join(" ");
- } else {
- const tp = {
- "api-key": params["api-key"],
- "output-branches": 1,
- "model-name": params["model-name"],
- "tokens-per-branch": 10,
- temperature: params["temperature"],
- "top-p": params["top-p"],
- "top-k": params["top-k"],
- repetition_penalty: params["repetition-penalty"],
- };
- let batch;
- if (params["sampler"] === "openai") {
- batch = await togetherGetResponses({
- endpoint: endpoint,
- prompt: prompt,
- togetherParams: tp,
- openai: true,
- });
- } else {
- batch = await togetherGetResponses({
- endpoint: endpoint,
- prompt: prompt,
- togetherParams: tp,
- });
- }
- return batch[0]["text"].trim().split("\n")[0].split(" ").slice(0,3).join(" ");
- }
-}
-
-async function rewriteNode(id) {
- const endpoint = document.getElementById("api-url").value;
- const rewriteNodePrompt = document.getElementById("rewrite-node-prompt");
- const rewritePromptPath = path.join(__dirname, "prompts", "rewrite.txt");
- const rewritePrompt = fs.readFileSync(rewritePromptPath, "utf8");
- const rewriteFeedback = rewriteNodePrompt.value;
- const rewriteContext = editor.value;
-
- // TODO: Add new endpoint? Make tokenizer that returns to client?
- // Could also make dedicated rewriteNode endpoint
- let tokens = document.getElementById("tokens-per-branch").value;
- const outputBranches = document.getElementById("output-branches").value;
-
- // Make sure we don't give too much or too little context
- // TODO: Change this once models have longer context/are less limited
- if (tokens < 256) {
- tokens = 256;
- } else if (tokens > 512) {
- tokens = 512;
- }
-
- let prompt = rewritePrompt.trim();
- prompt += rewriteContext.slice(-(tokens * 8)).trim();
- prompt += "\n\n";
- prompt += "Rewrite the text using the following feedback:\n";
- prompt += rewriteFeedback;
- prompt += "<|end|>";
-
- diceSetup();
- r = await fetch(endpoint + "generate", {
- method: "POST",
- body: JSON.stringify({
- prompt: prompt,
- prompt_node: false,
- adapter: "evaluator",
- evaluationPrompt: "",
- tokens_per_branch: tokens,
- output_branches: outputBranches,
- }),
- headers: {
- "Content-type": "application/json; charset=UTF-8",
- },
- });
- let batch = await r.json();
- console.log(batch);
-
- const focusParent = loomTree.nodeStore[focus.parent];
- const focusParentText = loomTree.renderNode(focusParent);
- for (i = 0; i < batch.length; i++) {
- let response = batch[i];
- let summary = await getSummary(response["text"]);
- const responseNode = loomTree.createNode(
- "rewrite",
- focus,
- focusParentText + response["text"],
- summary
- );
- loomTree.nodeStore[responseNode.id]["feedback"] = rewriteFeedback;
- loomTree.nodeStore[responseNode.id]["rewritePrompt"] = prompt;
- loomTree.nodeStore[responseNode.id]["model"] = response["base_model"];
- }
- const chatPane = document.getElementById("chat-pane");
- chatPane.innerHTML = "";
- diceTeardown();
- renderTick();
-}
-
-function promptRewriteNode(id) {
- const rewriteNodeLabel = document.createElement("label");
- rewriteNodeLabel.for = "rewrite-node-prompt";
- rewriteNodeLabel.textContent = "Rewrite Node From Feedback";
- const rewriteNodePrompt = document.createElement("textarea");
- rewriteNodePrompt.id = "rewrite-node-prompt";
- rewriteNodePrompt.value = "";
- rewriteNodePrompt.placeholder =
- "Write 3-5 bulletpoints of feedback to rewrite the node with.";
- const rewriteNodeSubmit = document.createElement("input");
- rewriteNodeSubmit.id = "rewrite-node-submit";
- rewriteNodeSubmit.type = "button";
- rewriteNodeSubmit.value = "Submit";
- rewriteNodeSubmit.onclick = () => rewriteNode(focus.id);
-
- const chatPane = document.getElementById("chat-pane");
- chatPane.append(rewriteNodeLabel, rewriteNodePrompt, rewriteNodeSubmit);
-}
-
-function promptThumbsUp(id) {
- loomTree.nodeStore[id].rating = true;
- promptBranchControls = document.getElementById("prompt-branch-controls");
- thumbUp = promptBranchControls.children.item(0).children.item(0);
- thumbUp.classList = ["chosen"];
- thumbDown = promptBranchControls.children.item(0).children.item(1);
- thumbDown.classList = ["thumbs"];
-}
-
-function promptThumbsDown(id) {
- loomTree.nodeStore[id].rating = false;
- promptBranchControls = document.getElementById("prompt-branch-controls");
- thumbUp = promptBranchControls.children.item(0).children.item(0);
- thumbUp.classList = ["thumbs"];
- thumbDown = promptBranchControls.children.item(0).children.item(1);
- thumbDown.classList = ["chosen"];
-}
-
-function diceSetup() {
- editor.readOnly = true;
- const diceHolder = document.getElementById("dice-holder");
- const die = document.createElement("p");
- die.innerText = "š²";
- die.id = "die";
- diceHolder.appendChild(die);
-}
-
-function diceTeardown() {
- editor.readOnly = false;
- const die = document.getElementById("die");
- die.remove();
-}
-
-// Roulette-style color scheme (alternating red and black)
-function getRouletteColor(index) {
- return index % 2 === 0 ? '#ef4444' : '#0f172a'; // bright red : very dark (almost black)
-}
-
-// Handle palette selection change
-function onPaletteChange(event) {
- const paletteName = event.target.value;
-
- if (!paletteName) {
- clearWheel();
- return;
- }
-
- const palette = samplerSettingsStore.palettes[paletteName];
- updateWheel(palette);
-}
-
-// Clear the wheel
-function clearWheel() {
- const wheelContainer = document.getElementById('wheelContainer');
- const wheelCenter = document.getElementById('wheelCenter');
- wheelContainer.classList.remove("active");
- wheelCenter.classList.remove("active");
- wheelCenter.style.display = "none";
- const wheel = document.getElementById('paletteWheel');
- wheel.innerHTML = 'Select palette
';
-
- const samplerSelect = document.getElementById('samplerItemSelect');
- samplerSelect.innerHTML = '';
-
- document.getElementById('currentSampler').textContent = 'None selected';
-}
-
-// Create SVG arc path
-function createArcPath(centerX, centerY, innerRadius, outerRadius, startAngle, endAngle) {
- const startAngleRad = (startAngle * Math.PI) / 180;
- const endAngleRad = (endAngle * Math.PI) / 180;
-
- const x1 = centerX + innerRadius * Math.cos(startAngleRad);
- const y1 = centerY + innerRadius * Math.sin(startAngleRad);
- const x2 = centerX + outerRadius * Math.cos(startAngleRad);
- const y2 = centerY + outerRadius * Math.sin(startAngleRad);
- const x3 = centerX + outerRadius * Math.cos(endAngleRad);
- const y3 = centerY + outerRadius * Math.sin(endAngleRad);
- const x4 = centerX + innerRadius * Math.cos(endAngleRad);
- const y4 = centerY + innerRadius * Math.sin(endAngleRad);
-
- const largeArc = endAngle - startAngle > 180 ? 1 : 0;
-
- return `M ${x1} ${y1} L ${x2} ${y2} A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x3} ${y3} L ${x4} ${y4} A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${x1} ${y1}`;
-}
-
-// Update the wheel with palette items
-function updateWheel(palette) {
- const wheelContainer = document.getElementById('wheelContainer');
- const wheelCenter = document.getElementById('wheelCenter');
- wheelContainer.classList.add("active");
- wheelCenter.classList.add("active");
- wheelCenter.style.display = "flex";
- const wheel = document.getElementById('paletteWheel');
- const samplerSelect = document.getElementById('samplerItemSelect');
- const items = Object.entries(palette);
- const numItems = items.length;
-
- if (numItems === 0) {
- clearWheel();
- return;
- }
-
- // Clear existing content
- wheel.innerHTML = '';
- samplerSelect.innerHTML = '';
-
- // Create SVG element
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- svg.setAttribute('class', 'wheel-svg');
- svg.setAttribute('viewBox', '0 0 100 100');
-
- const centerX = 50;
- const centerY = 50;
- const innerRadius = 18;
- const outerRadius = 48;
-
- // Add option for each sampler item to hidden select
- items.forEach(([name, data]) => {
- const option = document.createElement('option');
- option.value = name;
- option.textContent = name;
- samplerSelect.appendChild(option);
- });
-
- // Create wheel segments
- const anglePerSection = 360 / numItems;
-
- // Add outer gold ring
- const outerRing = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
- outerRing.setAttribute('cx', centerX);
- outerRing.setAttribute('cy', centerY);
- outerRing.setAttribute('r', outerRadius + 1);
- outerRing.setAttribute('fill', 'none');
- outerRing.setAttribute('stroke', '#fbbf24');
- outerRing.setAttribute('stroke-width', '2');
-
- // Add inner gold ring
- const innerRing = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
- innerRing.setAttribute('cx', centerX);
- innerRing.setAttribute('cy', centerY);
- innerRing.setAttribute('r', innerRadius - 1);
- innerRing.setAttribute('fill', 'none');
- innerRing.setAttribute('stroke', '#fbbf24');
- innerRing.setAttribute('stroke-width', '2');
-
- items.forEach(([name, data], index) => {
- const startAngle = index * anglePerSection - 90; // Start from top
- const endAngle = (index + 1) * anglePerSection - 90;
-
- // Create path for segment
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- path.setAttribute('d', createArcPath(centerX, centerY, innerRadius, outerRadius, startAngle, endAngle));
- path.setAttribute('fill', getRouletteColor(index));
- path.setAttribute('class', 'wheel-segment');
- path.dataset.samplerName = name;
-
- // Add click handler
- path.addEventListener('click', () => selectSamplerItem(name));
-
- svg.appendChild(path);
-
- // Add text label - only for smaller numbers of items
- if (numItems <= 4) {
- const midAngle = (startAngle + endAngle) / 2;
- const midAngleRad = (midAngle * Math.PI) / 180;
- const textRadius = (innerRadius + outerRadius) / 2;
- const textX = centerX + textRadius * Math.cos(midAngleRad);
- const textY = centerY + textRadius * Math.sin(midAngleRad);
-
- const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
- text.setAttribute('x', textX);
- text.setAttribute('y', textY);
- text.setAttribute('class', 'segment-text');
-
- // Very short labels
- const displayName = name.length > 8 ? name.substring(0, 6) + '..' : name.substring(0, 8);
- text.textContent = displayName;
-
- // Rotate text to align with segment
- const rotation = midAngle + 90 > 90 && midAngle + 90 < 270 ? midAngle + 270 : midAngle + 90;
- text.setAttribute('transform', `rotate(${rotation}, ${textX}, ${textY})`);
-
- svg.appendChild(text);
- }
- });
-
- wheel.appendChild(svg);
-
- // Add gold rings on top (after segments are added)
- svg.appendChild(outerRing);
- svg.appendChild(innerRing);
-
- // Select first item by default
- if (items.length > 0) {
- selectSamplerItem(items[0][0]);
- }
- }
-
-// Handle sampler item selection
-function selectSamplerItem(samplerName) {
- const samplerSelect = document.getElementById('samplerItemSelect');
- samplerSelect.value = samplerName;
-
- // Update visual feedback
- document.querySelectorAll('.wheel-segment').forEach(segment => {
- segment.classList.remove('active');
- if (segment.dataset.samplerName === samplerName) {
- segment.classList.add('active');
- }
- });
-
- // Update current selection display
- document.getElementById('currentSampler').textContent = "šļø" + samplerName;
-
- // Save choice for when we have to render palette wheel again
- // As well as when user closes app
- if (!("current-sampler" in samplerSettingsStore)) {
- samplerSettingsStore["current-sampler"] = Object();
- }
- const paletteName = document.getElementById("paletteSelect").value;
- samplerSettingsStore["current-sampler"]["palette"] = paletteName;
- samplerSettingsStore["current-sampler"]["sampler-item"] = samplerName;
-}
-
-async function delay(ms) {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
-async function togetherGetResponses({
- endpoint,
- prompt,
- togetherParams = {},
- api = "openai",
-}) {
- const tp = togetherParams;
- const auth_token = "Bearer " + tp["api-key"];
- const apiDelay = Number(tp["delay"]);
- let batch_promises = [];
- // Together doesn't let you get more than one completion at a time
- // But OpenAI expects you to use the n parameter
- let calls = api === "openai" ? 1 : tp["output-branches"];
- for (let i = 1; i <= calls; i++) {
- console.log("Together API called");
- const body = {
- model: tp["model-name"],
- prompt: prompt,
- max_tokens: Number(tp["tokens-per-branch"]),
- n: api === "openai" ? Number(tp["output-branches"]) : 1,
- temperature: Number(tp["temperature"]),
- top_p: Number(tp["top-p"]),
- top_k: Number(tp["top-k"]),
- repetition_penalty: Number(tp["repetition_penalty"]),
- };
- if (api === "openrouter") {
- body["provider"] = {};
- body["provider"]["require_parameters"] = true;
- }
- const promise = delay(apiDelay * i)
- .then(async () => {
- let r = await fetch(endpoint, {
- method: "POST",
- body: JSON.stringify(body),
- headers: {
- accept: "application/json",
- "Content-type": "application/json; charset=UTF-8",
- Authorization: auth_token,
- },
- });
- return r.json();
- })
- .then((response_json) => {
- let outs = [];
- let choices_length;
- if (api === "openai") {
- choices_length = response_json["choices"].length;
- } else if (api === "openrouter") {
- choices_length = response_json["choices"].length;
- } else {
- choices_length = response_json["output"]["choices"].length;
- }
- for (let i = 0; i < choices_length; i++) {
- if (api === "openai") {
- outs.push({
- text: response_json["choices"][i]["text"],
- model: response_json["model"],
- });
- } else if (api === "openrouter") {
- outs.push({
- text: response_json["choices"][i]["text"],
- model: response_json["model"],
- });
- } else {
- outs.push({
- text: response_json["output"]["choices"][i]["text"],
- model: response_json["model"],
- });
- }
- }
- if (api === "openai") {
- return outs;
- } else {
- return outs[0];
- }
- });
- batch_promises.push(promise);
- }
- let batch;
- if (api === "openai") {
- batch = await Promise.all(batch_promises);
- batch = batch[0];
- } else {
- batch = await Promise.all(batch_promises);
- }
- return batch;
-}
-
-async function reroll(id, weave = true) {
- const params = prepareRollParams();
- if (params["sampler"] === "base") {
- baseRoll(id, weave);
- } else if (params["sampler"] === "vae-guided") {
- await vaeGuidedRoll(id);
- } else if (params["sampler"] === "together") {
- togetherRoll(id, (api = "together"));
- } else if (params["sampler"] === "openrouter") {
- togetherRoll(id, (api = "openrouter"));
- } else if (params["sampler"] === "openai") {
- togetherRoll(id, (api = "openai"));
- } else if (params["sampler"] === "openai-chat") {
- await openaiChatCompletionsRoll(id);
- }
-}
-
-function readFileAsJson(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- const json = JSON.parse(e.target.result);
- resolve(json);
- } catch (error) {
- reject(error);
- }
- };
- reader.onerror = () => {
- reject(new Error("Error reading the file"));
- };
- reader.readAsText(file);
- });
-}
-
-async function togetherRoll(id, api = "openai") {
- diceSetup();
- await autoSaveTick();
- await updateFocusSummary();
- const rollFocus = loomTree.nodeStore[id];
- let prompt = loomTree.renderNode(rollFocus);
- const params = prepareRollParams();
-
-
- const apiDelay = params["api-delay"];
- const tp = {
- "api-key": params["api-key"],
- "model-name": params["model-name"],
- "output-branches": params["output-branches"],
- "tokens-per-branch": params["tokens-per-branch"],
- temperature: params["temperature"],
- "top-p": params["top-p"],
- "top-k": params["top-k"],
- repetition_penalty: params["repetition-penalty"],
- delay: apiDelay,
- };
- let newResponses;
- try {
- newResponses = await togetherGetResponses({
- endpoint: params["api-url"],
- prompt: prompt,
- togetherParams: tp,
- api: api,
- });
- } catch (error) {
- diceTeardown();
- errorMessage.textContent = "Error: " + error.message;
- console.warn(error);
- throw error;
- }
- for (let i = 0; i < newResponses.length; i++) {
- response = newResponses[i];
- const responseSummary = await delay(apiDelay).then(() => {
- return getSummary(response["text"]);
- });
- const childText = loomTree.renderNode(rollFocus) + response["text"];
- const responseNode = loomTree.createNode(
- "gen",
- rollFocus,
- childText,
- responseSummary
- );
- loomTree.nodeStore[responseNode.id]["model"] = response["model"];
- }
- focus = loomTree.nodeStore[rollFocus.children.at(-1)];
- diceTeardown();
- renderTick();
-}
-
-// Add this function for OpenAI Chat Completions API calls
-async function openaiChatCompletionsRoll(id) {
- diceSetup();
- await autoSaveTick();
- await updateFocusSummary();
- const rollFocus = loomTree.nodeStore[id];
- let promptText = loomTree.renderNode(rollFocus);
- const params = prepareRollParams();
-
- try {
- // Parse the JSON from the editor
- let chatData = JSON.parse(promptText);
-
- if (!chatData.messages || !Array.isArray(chatData.messages)) {
- throw new Error("Invalid chat format: messages array not found");
- }
-
- const apiKey = params["api-key"];
- const modelName = params["model-name"];
- const temperature = parseFloat(
- params["temperature"]
- );
- const topP = parseFloat(params["top-p"]);
- const outputBranches = parseInt(
- params["output-branches"]
- );
- const tokensPerBranch = parseInt(
- params["tokens-per-branch"]
- );
-
- // Prepare the API request
- const requestBody = {
- model: modelName,
- messages: chatData.messages,
- max_tokens: tokensPerBranch,
- temperature: temperature,
- top_p: topP,
- n: outputBranches,
- };
-
- // Make the API call
- const response = await fetch(params["api-url"], {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${apiKey}`,
- },
- body: JSON.stringify(requestBody),
- });
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(
- `OpenAI API Error: ${errorData.error?.message || response.statusText}`
- );
- }
-
- const responseData = await response.json();
-
- // Process each choice (for multiple outputs)
- for (let i = 0; i < responseData.choices.length; i++) {
- const choice = responseData.choices[i];
- const assistantMessage = choice.message;
-
- // Create a new chat data object with the assistant's response
- const newChatData = JSON.parse(JSON.stringify(chatData)); // Deep clone
- newChatData.messages.push({
- role: assistantMessage.role,
- content: assistantMessage.content,
- });
-
- const newChatText = JSON.stringify(newChatData, null, 2);
-
- // Generate a summary for the new node
- const summary = await getSummary(
- assistantMessage.content || "Assistant response"
- );
-
- // Create the new node
- const responseNode = loomTree.createNode(
- "gen",
- rollFocus,
- newChatText,
- summary
- );
-
- // Store metadata
- loomTree.nodeStore[responseNode.id]["model"] = responseData.model;
- loomTree.nodeStore[responseNode.id]["usage"] = responseData.usage;
- loomTree.nodeStore[responseNode.id]["finish_reason"] =
- choice.finish_reason;
- }
-
- // Focus on the last generated response
- focus = loomTree.nodeStore[rollFocus.children.at(-1)];
- } catch (error) {
- diceTeardown();
- errorMessage.textContent = "Error: " + error.message;
- console.error("OpenAI Chat Completions Error:", error);
- return;
- }
-
- diceTeardown();
- renderTick();
-}
-
-function countCharacters(text) {
- return text.length;
-}
-
-function countWords(text) {
- return text
- .trim()
- .split(/\s+/)
- .filter((word) => word.length > 0).length;
-}
-
-function updateCounterDisplay(text) {
- const charCount = countCharacters(text);
- const wordCount = countWords(text);
-
- promptTokenCounter.innerText = `${wordCount} Words (${charCount} Characters)`;
-}
-
-var secondsSinceLastTyped = 0;
-var updatingNode = false;
-
-editor.addEventListener("input", async (e) => {
- const prompt = editor.value;
- // Autosave users work when writing next prompt
- if (
- focus.children.length > 0 ||
- ["gen", "rewrite", "root"].includes(focus.type)
- ) {
- const child = loomTree.createNode("user", focus, prompt, "New Node");
- changeFocus(child.id);
- }
-});
-
-editor.addEventListener("keydown", async (e) => {
- secondsSinceLastTyped = 0;
- const prompt = editor.value;
- const params = prepareRollParams();
-
- if (focus.children.length == 0 && focus.type == "user" && !updatingNode) {
- updatingNode = true;
- loomTree.updateNode(focus, prompt, focus.summary);
- updatingNode = false;
- }
-
- if (e.key != "Enter") {
- // Update character/word count on every keystroke
- updateCounterDisplay(prompt);
-
- if (prompt.length % 32 == 0) {
- // Removed the fetch call to check-tokens endpoint
-
- // Update summary while user is writing next prompt
- if (
- focus.children.length == 0 &&
- focus.type == "user" &&
- [
- "base",
- "vae-base",
- "vae-guided",
- "vae-paragraph",
- "vae-bridge",
- ].includes(params["sampler"]) &&
- !updatingNode
- ) {
- try {
- updatingNode = true;
- const summary = await getSummary(prompt);
- loomTree.updateNode(focus, prompt, summary);
- updatingNode = false;
- } catch (error) {
- console.log(error);
- updatingNode = false;
- }
- }
- // Render only the loom tree so we don't interrupt their typing
- loomTreeView.innerHTML = "";
- renderTree(focus, loomTreeView);
- }
- return null;
- } else if (!e.shiftKey) {
- return null;
- }
- reroll(focus.id, settingUseWeave.checked);
-});
-
-function saveFile() {
- const data = {
- loomTree,
- focus: focus,
- };
- ipcRenderer
- .invoke("save-file", data)
- .catch((err) => console.error("Save File Error:", err));
-}
-
-function loadFile() {
- return ipcRenderer
- .invoke("load-file")
- .then((data) => {
- loomTreeRaw = data.loomTree;
- loomTree = Object.assign(new LoomTree(), loomTreeRaw);
- focus = loomTree.nodeStore[data.focus.id];
- renderTick();
- })
- .catch((err) => console.error("Load File Error:", err));
-}
-
-function autoSave() {
- const data = {
- loomTree,
- focus: focus,
- samplerSettingsStore: samplerSettingsStore,
- };
- ipcRenderer
- .invoke("auto-save", data)
- .catch((err) => console.error("Auto-save Error:", err));
-}
-
-var secondsSinceLastSave = 0;
-async function autoSaveTick() {
- secondsSinceLastSave += 1;
- secondsSinceLastTyped += 1;
- if (secondsSinceLastSave == 30 || secondsSinceLastSave > 40) {
- autoSave();
- secondsSinceLastSave = 0;
- }
-}
-
-
-const onSettingsUpdated = async () => {
- samplerSettingsStore = ipcRenderer
- .invoke("load-settings")
- .then((data) => {
- if (data != null) {
- samplerSettingsStore = data;
- }
- })
- .catch((err) => console.error("Load Settings Error:", err));
-}
-
-// attach once on startup
-document.addEventListener("DOMContentLoaded", () => {
- ipcRenderer.on("settings-updated", onSettingsUpdated);
-});
-
-async function updateFocusSummary() {
- if (focus.type == "user" && focus.children.length == 0 && !updatingNode) {
- const currentFocus = focus; // Stop focus from changing out underneath us
- const newPrompt = editor.value;
- const prompt = loomTree.renderNode(currentFocus);
- updatingNode = true;
- try {
- let summary = await getSummary(prompt);
- if (summary.trim() === "") {
- summary = "Summary Not Given";
- }
- loomTree.updateNode(currentFocus, newPrompt, summary);
- } catch (error) {
- loomTree.updateNode(currentFocus, newPrompt, "Server Response Error");
- }
- updatingNode = false;
- }
-}
-
-var autoSaveIntervalId = setInterval(autoSaveTick, 1000);
-
-ipcRenderer.on("invoke-action", (event, action) => {
- switch (action) {
- case "save-file":
- saveFile();
- break;
- case "load-file":
- loadFile();
- break;
- default:
- console.log("Action not recognized", action);
- }
-});
-
-// Helper function to validate chat JSON
-function isValidChatJson(text) {
- try {
- const data = JSON.parse(text);
- return data.messages && Array.isArray(data.messages);
- } catch (e) {
- return false;
- }
-}
-
-editor.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- ipcRenderer.send("show-context-menu");
-});
-
-let samplerSettingsStore;
-
-function loadSettings() {
- return ipcRenderer
- .invoke("load-settings")
- .then((data) => {
- if (data != null) {
- samplerSettingsStore = data;
- }
- })
- .catch((err) => console.error("Load Settings Error:", err));
-}
-
-async function init() {
- await loadSettings();
- renderTick();
-}
-init();
-updateCounterDisplay(editor.value || "");
\ No newline at end of file
diff --git a/sampler-settings.js b/sampler-settings.js
deleted file mode 100644
index 9ab3b68..0000000
--- a/sampler-settings.js
+++ /dev/null
@@ -1,880 +0,0 @@
-// TODO: Use a preload
-const { ipcRenderer } = require("electron");
-const samplerOptionMenu = document.getElementById("menu-host");
-const menuHost = samplerOptionMenu;
-let samplerSettingsStore = {};
-
-function baseSamplerMenu() {
- samplerOptionMenu.innerHTML = "";
-
- const settingsNameLabel = document.createElement("label");
- settingsNameLabel.for = "setting-settings-name";
- settingsNameLabel.textContent = "Settings Name";
- const settingsName = document.createElement("input");
- settingsName.type = "text";
- settingsName.id = "setting-settings-name";
- settingsName.name = "setting-settings-name";
- settingsName.classList.add("settingsNameType");
- settingsName.value = "Default";
-
- const apiUrlLabel = document.createElement("label");
- apiUrlLabel.for = "api-url";
- apiUrlLabel.textContent = "API URL";
- const apiUrl = document.createElement("input");
- apiUrl.type = "text";
- apiUrl.id = "api-url";
- apiUrl.name = "api-url";
- apiUrl.classList.add("URLType");
- apiUrl.value = "http://localhost:5000/";
-
- const outputBranchesLabel = document.createElement("label");
- outputBranchesLabel.for = "output-branches";
- outputBranchesLabel.classList.add("first-sampler-menu-item");
- outputBranchesLabel.textContent = "Output Branches";
- const outputBranches = document.createElement("input");
- outputBranches.type = "text";
- outputBranches.id = "output-branches";
- outputBranches.name = "output-branches";
- outputBranches.classList.add("intType");
- outputBranches.value = "2";
-
- const tokensPerBranchLabel = document.createElement("label");
- tokensPerBranchLabel.for = "tokens-per-branch";
- tokensPerBranchLabel.textContent = "Tokens Per Branch";
- const tokensPerBranch = document.createElement("input");
- tokensPerBranch.type = "text";
- tokensPerBranch.id = "tokens-per-branch";
- tokensPerBranch.name = "tokens-per-branch";
- tokensPerBranch.classList.add("intType");
- tokensPerBranch.value = "256";
-
- const temperatureLabel = document.createElement("label");
- temperatureLabel.for = "temperature";
- temperatureLabel.textContent = "Temperature";
- const temperature = document.createElement("input");
- temperature.type = "text";
- temperature.id = "temperature";
- temperature.name = "temperature";
- temperature.classList.add("floatType");
- temperature.value = "0.9";
-
- const topPLabel = document.createElement("label");
- topPLabel.for = "top-p";
- topPLabel.textContent = "Top-P";
- const topP = document.createElement("input");
- topP.type = "text";
- topP.id = "top-p";
- topP.name = "top-p";
- topP.classList.add("floatType");
- topP.value = "1";
-
- const topKLabel = document.createElement("label");
- topKLabel.for = "top-k";
- topKLabel.textContent = "Top-K";
- const topK = document.createElement("input");
- topK.type = "text";
- topK.id = "top-k";
- topK.name = "top-k";
- topK.classList.add("intType");
- topK.value = "100";
-
- const repetitionPenaltyLabel = document.createElement("label");
- repetitionPenaltyLabel.for = "repetition-penalty";
- repetitionPenaltyLabel.textContent = "Repetition Penalty";
- const repetitionPenalty = document.createElement("input");
- repetitionPenalty.type = "text";
- repetitionPenalty.id = "repetition-penalty";
- repetitionPenalty.name = "repetition-penalty";
- repetitionPenalty.classList.add("floatType");
- repetitionPenalty.value = "1";
-
- const apiDelayLabel = document.createElement("label");
- apiDelayLabel.for = "api-delay";
- apiDelayLabel.textContent = "API Delay (milliseconds)";
- const apiDelay = document.createElement("input");
- apiDelay.type = "text";
- apiDelay.id = "api-delay";
- apiDelay.name = "api-delay";
- apiDelay.classList.add("intType");
- apiDelay.value = 3000;
-
- const modelNameLabel = document.createElement("label");
- modelNameLabel.for = "model-name";
- modelNameLabel.textContent = "Model Name";
- const modelName = document.createElement("input");
- modelName.type = "text";
- modelName.id = "model-name";
- modelName.name = "model-name";
- modelName.classList.add("modelNameType");
- modelName.value = "togethercomputer/llama-2-70b";
-
- samplerOptionMenu.append(settingsNameLabel);
- samplerOptionMenu.append(settingsName);
- samplerOptionMenu.append(apiUrlLabel);
- samplerOptionMenu.append(apiUrl);
- samplerOptionMenu.append(outputBranchesLabel);
- samplerOptionMenu.append(outputBranches);
- samplerOptionMenu.append(tokensPerBranchLabel);
- samplerOptionMenu.append(tokensPerBranch);
- samplerOptionMenu.append(temperatureLabel);
- samplerOptionMenu.append(temperature);
- samplerOptionMenu.append(topPLabel);
- samplerOptionMenu.append(topP);
- samplerOptionMenu.append(topKLabel);
- samplerOptionMenu.append(topK);
- samplerOptionMenu.append(repetitionPenaltyLabel);
- samplerOptionMenu.append(repetitionPenalty);
- samplerOptionMenu.append(apiDelayLabel);
- samplerOptionMenu.append(apiDelay);
- samplerOptionMenu.append(modelNameLabel);
- samplerOptionMenu.append(modelName);
-}
-
-function togetherSamplerMenu() {
- baseSamplerMenu();
- const apiUrl = document.getElementById("api-url");
- apiUrl.value = "https://api.together.xyz/inference";
- const topP = document.getElementById("top-p");
- topP.value = "1";
- const topK = document.getElementById("top-k");
- topK.value = "100";
- const repetitionPenalty = document.getElementById("repetition-penalty");
- repetitionPenalty.value = "1";
- const apiDelay = document.getElementById("api-delay");
- apiDelay.value = 3000;
- const modelName = document.getElementById("model-name");
- modelName.value = "togethercomputer/llama-2-70b";
-}
-
-function openrouterSamplerMenu() {
- baseSamplerMenu();
- const apiUrl = document.getElementById("api-url");
- apiUrl.value = "https://openrouter.ai/api/v1/chat/completions";
- const topP = document.getElementById("top-p");
- topP.value = "1";
- const topK = document.getElementById("top-k");
- topK.value = "100";
- const repetitionPenalty = document.getElementById("repetition-penalty");
- repetitionPenalty.value = "1";
- const apiDelay = document.getElementById("api-delay");
- apiDelay.value = 3000;
- const modelName = document.getElementById("model-name");
- modelName.value = "deepseek/deepseek-v3-base:free";
-}
-
-function openaiCompletionsSamplerMenu() {
- baseSamplerMenu();
- const apiUrl = document.getElementById("api-url");
- apiUrl.value = "https://api.openai.com/";
- const topP = document.getElementById("top-p");
- topP.value = "1";
- const topK = document.getElementById("top-k");
- topK.value = "100";
- const repetitionPenalty = document.getElementById("repetition-penalty");
- repetitionPenalty.value = "1";
- const apiDelay = document.getElementById("api-delay");
- apiDelay.value = 3000;
- const modelName = document.getElementById("model-name");
- modelName.value = "code-davinci-002";
-}
-
-// Add this function for the OpenAI Chat Completions sampler menu
-function openaiChatCompletionsSamplerMenu() {
- baseSamplerMenu();
- const apiUrl = document.getElementById("api-url");
- apiUrl.value = "https://api.openai.com/v1/chat/completions";
- const topP = document.getElementById("top-p");
- topP.value = "1";
- const topK = document.getElementById("top-k");
- topK.value = "100";
- const repetitionPenalty = document.getElementById("repetition-penalty");
- repetitionPenalty.value = "1";
- const apiDelay = document.getElementById("api-delay");
- apiDelay.value = 3000;
- const modelName = document.getElementById("model-name");
- modelName.value = "gpt-5";
-}
-
-function samplerMenuToDict() {
- const out = {};
- for (let child of samplerOptionMenu.children) {
- if (child.tagName === "INPUT" && child.id !== "setting-api-key") {
- out[child.id] = {"value": child.value, "type": child.className};
- }
- }
- return out;
-}
-
-// From Google AI overview for "node js url validation"
-function isValidUrl(urlString) {
- try {
- new URL(urlString);
- return true;
- } catch (error) {
- return false;
- }
-}
-
-function validateFieldStringType(fieldValue, fieldType) {
- const fieldValueString = String(fieldValue);
- const intPattern = /^[0-9]+$/
- const floatPattern = /^[0-9]+\.?[0-9]*$/
- const modelNamePattern = /^[a-zA-Z0-9-\._]+$/
- let result;
- if (fieldType === "intType") {
- result = fieldValueString.match(intPattern);
- }
- else if (fieldType === "floatType") {
- result = fieldValueString.match(floatPattern);
- }
- else if (fieldType === "modelNameType") {
- result = fieldValueString.match(modelNamePattern);
- }
- else if (fieldType === "settingsNameType") {
- result = fieldValueString.match(modelNamePattern);
- }
- else if (fieldType === "URLType") {
- result = isValidUrl(fieldValue);
- }
- else {
- if (fieldType === "") {
- console.warn("Tried to validate empty field type. Did you forget to type your form field?");
- result = null;
- }
- else {
- console.warn("Attempted to validate unknown field type")
- result = null;
- }
- }
- return result;
-}
-
-function loadSamplerMenuDict(samplerMenuDict) {
- for (let field of samplerOptionMenu.children) {
- if (field.id !== "setting-api-key" &&
- Object.keys(samplerMenuDict).includes(field.id) &&
- field.className === samplerMenuDict[field.id]["type"]) {
- if (validateFieldStringType(samplerMenuDict[field.id]["value"], field.className)) {
- field.value = samplerMenuDict[field.id]["value"];
- }
- else {
- throw new TypeError("Attempted to import bad sampler settings, is your JSON corrupted?");
- }
- }
- }
-}
-
-// Add this function to create a default chat JSON structure
-function createDefaultChatJson() {
- return JSON.stringify(
- {
- messages: [
- {
- role: "system",
- content: "You are a helpful assistant.",
- },
- {
- role: "user",
- content: "Hello!",
- },
- ],
- },
- null,
- 2
- );
-}
-
-function internalSaveSamplerSettings() {
- let currentSampler = document.getElementById("sampler").value;
- let settingsName = document.getElementById("setting-settings-name").value;
- if (Object.keys(samplerSettingsStore).includes("sampler-settings")) {
- samplerSettingsStore["sampler-settings"][settingsName] = samplerMenuToDict();
- }
- else {
- samplerSettingsStore["sampler-settings"] = new Object();
- samplerSettingsStore["sampler-settings"][settingsName] = samplerMenuToDict();
- }
- console.log(samplerSettingsStore);
- ipcRenderer
- .invoke("save-settings", samplerSettingsStore)
- .catch((err) => console.error("Settings save Error:", err));
-}
-
-// samplerOptionMenu.addEventListener("change", internalSaveSamplerSettings);
-// sampler.addEventListener("focus", internalSaveSamplerSettings);
-
-sampler?.addEventListener("change", function () {
- if (activeTab === "sampler-settings") {
- renderSamplerSettingsTab();
- }
-});
-
-function loadSettings() {
- return ipcRenderer
- .invoke("load-settings")
- .then((data) => {
- if (data != null) {
- samplerSettingsStore = data;
- }
- })
- .catch((err) => console.error("Load Settings Error:", err));
-}
-
-
-
-// ---------- Tab 1: Sampler Settings ----------
-function renderSamplerSettingsTab() {
- const samplerLabel = document.getElementById("sampler-label");
- samplerLabel.style.display = "initial";
- sampler.style.display = "initial";
- menuHost.innerHTML = "";
-
- // Use whatever sampler is currently selected
- const selected = sampler?.value || "openai";
- if (selected === "base") baseSamplerMenu();
- else if (selected === "together") togetherSamplerMenu();
- else if (selected === "openrouter") openrouterSamplerMenu();
- else if (selected === "openai") openaiCompletionsSamplerMenu();
- else if (selected === "openai-chat") {
- openaiChatCompletionsSamplerMenu();
- // Guard optional editor helpers if present
- if (typeof editor !== "undefined" && typeof isValidChatJson === "function") {
- if (!editor.value?.trim() || !isValidChatJson(editor.value)) {
- editor.value = createDefaultChatJson();
- if (typeof updateCounterDisplay === "function") updateCounterDisplay(editor.value);
- }
- }
- } else {
- // Fallback to base
- baseSamplerMenu();
- }
-
- if ("sampler-settings" in samplerSettingsStore &&
- "Default" in samplerSettingsStore["sampler-settings"]) {
- loadSamplerMenuDict(samplerSettingsStore["sampler-settings"]["Default"]);
- }
-
- // Divider + explicit SAVE UI (no more forced "Default" load)
- const divider = document.createElement("div");
- divider.className = "divider";
- menuHost.appendChild(divider);
-
- // Save row: use your existing #setting-settings-name as the preset name
- const saveRow = document.createElement("div");
- saveRow.className = "row";
- const saveBtn = document.createElement("button");
- saveBtn.type = "button";
- saveBtn.className = "btn primary";
- saveBtn.textContent = "Save preset";
- saveBtn.title = "Save sampler settings under this name";
- saveBtn.addEventListener("click", () => {
- try {
- internalSaveSamplerSettings();
- flashSaved("Preset saved.");
- } catch (e) {
- console.error(e);
- flashSaved("Failed to save preset (see console).", true);
- }
- });
-
- const hint = document.createElement("span");
- hint.className = "muted";
- hint.textContent = "Tip: change āSettings Nameā above, then click Save.";
-
- saveRow.appendChild(saveBtn);
- saveRow.appendChild(hint);
- menuHost.appendChild(saveRow);
-}
-
-function flashSaved(msg, isError = false) {
- const note = document.createElement("div");
- note.textContent = msg;
- note.style.marginTop = "8px";
- note.style.fontWeight = "600";
- note.style.color = isError ? "#b00020" : "#0a7d06";
- const pane = document.getElementById("settings-pane");
- pane.appendChild(note);
- setTimeout(() => note.remove(), 1800);
-}
-
-
-
-// ---------- Tab 2: API Keys ----------
-/**
- * Store shape:
- * samplerSettingsStore["api-keys"] = {
- * "OPENAI": "sk-...",
- * "TOGETHER": "tkn-..."
- * }
- */
-function getApiKeysObject() {
- if (!samplerSettingsStore["api-keys"]) {
- samplerSettingsStore["api-keys"] = {};
- }
- return samplerSettingsStore["api-keys"];
-}
-
-function persistStore() {
- return ipcRenderer
- .invoke("save-settings", samplerSettingsStore)
- .catch((err) => console.error("Settings save Error:", err));
-}
-
-function renderApiKeysTab() {
- const samplerLabel = document.getElementById("sampler-label");
- samplerLabel.style.display = "none";
- sampler.style.display = "none";
- menuHost.innerHTML = "";
-
- // Add Key form
- const addForm = document.createElement("div");
- addForm.className = "row";
- addForm.innerHTML = `
- Key Name
-
- Secret
-
- Add
- `;
- menuHost.appendChild(addForm);
-
- const warn = document.createElement("div");
- warn.className = "muted";
- warn.style.marginTop = "6px";
- warn.textContent = "Names allow AāZ, aāz, 0ā9, dash, underscore, and dot.";
- menuHost.appendChild(warn);
-
- const divider = document.createElement("div");
- divider.className = "divider";
- menuHost.appendChild(divider);
-
- // Keys table
- const table = document.createElement("table");
- table.className = "keys mono";
- table.innerHTML = `
-
-
- Name
- Secret
- Actions
-
-
-
- `;
- menuHost.appendChild(table);
-
- // Render rows
- function refreshKeysTable() {
- const tbody = table.querySelector("#keys-tbody");
- tbody.innerHTML = "";
- const obj = getApiKeysObject();
- const entries = Object.entries(obj);
- if (entries.length === 0) {
- const tr = document.createElement("tr");
- const td = document.createElement("td");
- td.colSpan = 3;
- td.className = "muted";
- td.textContent = "No API keys saved.";
- tr.appendChild(td);
- tbody.appendChild(tr);
- return;
- }
- for (const [name, secret] of entries) {
- const tr = document.createElement("tr");
- const tdName = document.createElement("td");
- tdName.textContent = name;
-
- const tdSecret = document.createElement("td");
- const mask = document.createElement("span");
- mask.textContent = "ā¢".repeat(Math.min(secret?.length || 0, 12)) || "ā";
- mask.dataset.revealed = "0";
- tdSecret.appendChild(mask);
-
- const tdActions = document.createElement("td");
- const showBtn = document.createElement("button");
- showBtn.className = "btn";
- showBtn.textContent = "Show";
- showBtn.addEventListener("click", () => {
- const revealed = mask.dataset.revealed === "1";
- mask.textContent = revealed ? "ā¢".repeat(Math.min(secret?.length || 0, 12)) : (secret || "");
- mask.dataset.revealed = revealed ? "0" : "1";
- showBtn.textContent = revealed ? "Show" : "Hide";
- });
-
- const delBtn = document.createElement("button");
- delBtn.className = "btn";
- delBtn.style.marginLeft = "6px";
- delBtn.textContent = "Delete";
- delBtn.addEventListener("click", async () => {
- const obj = getApiKeysObject();
- delete obj[name];
- await persistStore();
- refreshKeysTable();
- });
-
- tdActions.appendChild(showBtn);
- tdActions.appendChild(delBtn);
-
- tr.appendChild(tdName);
- tr.appendChild(tdSecret);
- tr.appendChild(tdActions);
- tbody.appendChild(tr);
- }
- }
-
- // Add key handler
- addForm.querySelector("#add-key-btn").addEventListener("click", async () => {
- const nameEl = addForm.querySelector("#key-label");
- const valEl = addForm.querySelector("#key-value");
- const name = nameEl.value.trim();
- const value = valEl.value;
-
- // Reuse your validator
- if (!validateFieldStringType(name, "modelNameType")) {
- alert("Invalid key name. Use letters, digits, '-', '_', or '.'.");
- return;
- }
- if (!value) {
- alert("Secret cannot be empty.");
- return;
- }
-
- const obj = getApiKeysObject();
- obj[name] = value;
- await persistStore();
- nameEl.value = "";
- valEl.value = "";
- refreshKeysTable();
- });
-
- refreshKeysTable();
-}
-
-function renderPalettesTab() {
- const host = samplerOptionMenu;
- host.innerHTML = "";
-
- // ---- helpers ----
- function palettesObj() {
- if (!samplerSettingsStore.palettes) samplerSettingsStore.palettes = {};
- return samplerSettingsStore.palettes;
- }
- const samplerTypes = [
- { value: "openai", label: "OpenAI Completions" },
- { value: "openai-chat", label: "OpenAI Chat Completions" },
- { value: "openrouter", label: "OpenRouter API" },
- { value: "together", label: "Together API" },
- ];
- const listPresets = () => Object.keys(samplerSettingsStore["sampler-settings"] || {});
- const listApiKeys = () => Object.keys(samplerSettingsStore["api-keys"] || {});
- const persist = () =>
- ipcRenderer.invoke("save-settings", samplerSettingsStore)
- .catch(err => console.error("Settings save Error:", err));
-
- // ---- top row: palette picker + inline name field with Create/Rename/Delete ----
- const topRow = document.createElement("div"); topRow.className = "row";
-
- const palLabel = document.createElement("label");
- palLabel.textContent = "Palette";
-
- const palSelect = document.createElement("select");
- palSelect.id = "paletteEditorSelect";
-
- // ensure at least one palette exists
- const pObj = palettesObj();
- if (Object.keys(pObj).length === 0) pObj["MyFirstPalette"] = {};
-
- for (const name of Object.keys(pObj)) {
- const opt = document.createElement("option");
- opt.value = name; opt.textContent = name;
- palSelect.appendChild(opt);
- }
-
- const nameInput = document.createElement("input");
- nameInput.type = "text";
- nameInput.placeholder = "Palette name";
-
- const createBtn = document.createElement("button");
- createBtn.type = "button";
- createBtn.textContent = "Create";
-
- const renameBtn = document.createElement("button");
- renameBtn.type = "button";
- renameBtn.textContent = "Rename";
-
- const deleteBtn = document.createElement("button");
- deleteBtn.type = "button";
- deleteBtn.textContent = "Delete";
-
- topRow.append(palLabel, palSelect, nameInput, createBtn, renameBtn, deleteBtn);
- host.appendChild(topRow);
-
- const divider = document.createElement("div");
- divider.style.height = "1px";
- divider.style.background = "rgba(0,0,0,.1)";
- divider.style.margin = "8px 0";
- host.appendChild(divider);
-
- // ---- table: up to 6 items ----
- const table = document.createElement("table");
- table.style.width = "100%";
- table.style.borderCollapse = "collapse";
- table.innerHTML = `
-
-
- Item Name
- Sampler Type
- Sampler Preset
- API Key
- Actions
-
-
-
- `;
- host.appendChild(table);
-
- const controls = document.createElement("div"); controls.className = "row";
- const addBtn = document.createElement("button"); addBtn.type = "button"; addBtn.textContent = "Add Item";
- const saveBtn = document.createElement("button"); saveBtn.type = "button"; saveBtn.textContent = "Save Palette";
- controls.append(addBtn, saveBtn);
- host.appendChild(controls);
-
- const tbody = table.querySelector("#palItemsBody");
-
- function tdStyled() {
- const td = document.createElement("td");
- td.style.padding = "6px";
- td.style.borderBottom = "1px solid rgba(0,0,0,.08)";
- return td;
- }
-
- function fillRow(itemName = "", data = null) {
- const tr = document.createElement("tr");
-
- // name
- const tdName = tdStyled();
- const name = document.createElement("input");
- name.type = "text";
- name.placeholder = "e.g. FastDraft";
- name.value = itemName;
- tdName.appendChild(name);
-
- // sampler type
- const tdType = tdStyled();
- const typeSel = document.createElement("select");
- samplerTypes.forEach(t => {
- const o = document.createElement("option");
- o.value = t.value; o.textContent = t.label;
- typeSel.appendChild(o);
- });
- typeSel.value = data?.samplerType || samplerTypes[0].value;
- tdType.appendChild(typeSel);
-
- // preset
- const tdPreset = tdStyled();
- const presetSel = document.createElement("select");
- const presets = listPresets();
- if (presets.length === 0) {
- const o = document.createElement("option");
- o.value = ""; o.textContent = "(no presets)";
- presetSel.appendChild(o);
- } else {
- presets.forEach(n => {
- const o = document.createElement("option");
- o.value = n; o.textContent = n;
- presetSel.appendChild(o);
- });
- }
- presetSel.value = data?.samplerPreset || presetSel.options[0]?.value || "";
- tdPreset.appendChild(presetSel);
-
- // api key
- const tdKey = tdStyled();
- const keySel = document.createElement("select");
- const keys = listApiKeys();
- if (keys.length === 0) {
- const o = document.createElement("option");
- o.value = ""; o.textContent = "(no keys)";
- keySel.appendChild(o);
- } else {
- keys.forEach(k => {
- const o = document.createElement("option");
- o.value = k; o.textContent = k;
- keySel.appendChild(o);
- });
- }
- keySel.value = data?.apiKey || keySel.options[0]?.value || "";
- tdKey.appendChild(keySel);
-
- // actions
- const tdAct = tdStyled();
- const del = document.createElement("button");
- del.type = "button";
- del.textContent = "Delete";
- del.addEventListener("click", () => {
- tr.remove(); updateAddDisabled();
- });
- tdAct.appendChild(del);
-
- tr.append(tdName, tdType, tdPreset, tdKey, tdAct);
- tbody.appendChild(tr);
- }
-
- function updateAddDisabled() {
- addBtn.disabled = tbody.querySelectorAll("tr").length >= 6;
- }
-
- function loadPaletteIntoTable(paletteName) {
- tbody.innerHTML = "";
- const pal = palettesObj()[paletteName] || {};
- const entries = Object.entries(pal).slice(0, 6);
- if (entries.length === 0) {
- fillRow("", null); // start with one blank row for UX
- } else {
- for (const [itemName, data] of entries) fillRow(itemName, data);
- }
- updateAddDisabled();
- }
-
- function readFromTable() {
- const rows = Array.from(tbody.querySelectorAll("tr"));
- const out = {};
- for (const tr of rows) {
- const [nameInput, typeSel, presetSel, keySel] = tr.querySelectorAll("input,select");
- const itemName = (nameInput.value || "").trim();
- if (!itemName) continue; // ignore empty row(s)
- if (!validateFieldStringType(itemName, "modelNameType")) {
- throw new Error(`Invalid item name "${itemName}" (use letters/digits/._-).`);
- }
- if (out[itemName]) throw new Error(`Duplicate item name "${itemName}".`);
- out[itemName] = {
- samplerType: typeSel.value,
- samplerPreset: presetSel.value,
- apiKey: keySel.value,
- };
- }
- if (Object.keys(out).length > 6) throw new Error("A palette may contain at most six items.");
- return out;
- }
-
- // ---- wire top controls (no prompt/confirm) ----
- createBtn.addEventListener("click", async () => {
- const n = nameInput.value.trim();
- if (!n) return alert("Enter a palette name in the input field.");
- if (!validateFieldStringType(n, "modelNameType")) return alert("Use letters/digits/._-");
- const p = palettesObj();
- if (p[n]) return alert("A palette with that name already exists.");
- p[n] = {};
- await persist();
-
- // refresh select
- palSelect.appendChild(new Option(n, n));
- palSelect.value = n;
- nameInput.value = "";
- loadPaletteIntoTable(n);
- if (typeof flashSaved === "function") flashSaved("Palette created.");
- });
-
- renameBtn.addEventListener("click", async () => {
- const oldName = palSelect.value;
- const n = nameInput.value.trim();
- if (!n) return alert("Enter a new name in the input field.");
- if (!validateFieldStringType(n, "modelNameType")) return alert("Use letters/digits/._-");
- const p = palettesObj();
- if (p[n]) return alert("A palette with that name already exists.");
- p[n] = p[oldName]; delete p[oldName];
- await persist();
-
- // rebuild select (simpler than fiddling options)
- palSelect.innerHTML = "";
- for (const name of Object.keys(p)) palSelect.appendChild(new Option(name, name));
- palSelect.value = n;
- nameInput.value = "";
- loadPaletteIntoTable(n);
- if (typeof flashSaved === "function") flashSaved("Palette renamed.");
- });
-
- // Delete uses a small 2-step inline confirmation (no confirm())
- let deleteArmed = false, deleteTimer = null;
- deleteBtn.addEventListener("click", async () => {
- if (!deleteArmed) {
- deleteArmed = true;
- const oldText = deleteBtn.textContent;
- deleteBtn.textContent = "Click again to deleteā¦";
- clearTimeout(deleteTimer);
- deleteTimer = setTimeout(() => {
- deleteArmed = false;
- deleteBtn.textContent = oldText;
- }, 2500);
- return;
- }
- deleteArmed = false;
- deleteBtn.textContent = "Delete";
-
- const p = palettesObj();
- const current = palSelect.value;
- delete p[current];
- if (Object.keys(p).length === 0) p["MyFirstPalette"] = {};
-
- await persist();
- // rebuild select
- palSelect.innerHTML = "";
- for (const name of Object.keys(p)) palSelect.appendChild(new Option(name, name));
- loadPaletteIntoTable(palSelect.value);
- if (typeof flashSaved === "function") flashSaved("Palette deleted.");
- });
-
- // ---- items controls ----
- palSelect.addEventListener("change", () => loadPaletteIntoTable(palSelect.value));
-
- addBtn.addEventListener("click", () => {
- if (tbody.querySelectorAll("tr").length >= 6) return;
- fillRow("", null);
- updateAddDisabled();
- });
-
- saveBtn.addEventListener("click", async (e) => {
- e.preventDefault();
- try {
- const name = palSelect.value;
- palettesObj()[name] = readFromTable();
- await persist();
- if (typeof flashSaved === "function") flashSaved("Palette saved.");
- } catch (err) {
- console.error(err);
- alert(err.message || "Failed to save palette.");
- }
- });
-
- // ---- initial load ----
- loadPaletteIntoTable(palSelect.value);
-}
-
-
-function setActiveTab(tabName) {
- activeTab = tabName;
- for (const b of document.querySelectorAll("#settings-tabs .tab-btn")) {
- b.classList.toggle("active", b.dataset.tab === activeTab);
- }
- if (activeTab === "sampler-settings") {
- renderSamplerSettingsTab();
- } else if (activeTab === "palettes") renderPalettesTab();
- else {
- renderApiKeysTab();
- }
-}
-
-const tabs = document.getElementById("settings-tabs");
-tabs.addEventListener("click", (e) => {
- const btn = e.target.closest(".tab-btn");
- if (!btn) return;
- setActiveTab(btn.dataset.tab);
-});
-
-renderSamplerSettingsTab();
-loadSettings().then(() => {
- if ("sampler-settings" in samplerSettingsStore &&
- "Default" in samplerSettingsStore["sampler-settings"]) {
- loadSamplerMenuDict(samplerSettingsStore["sampler-settings"]["Default"]);
- }
-});
\ No newline at end of file
diff --git a/settings.html b/settings.html
deleted file mode 100644
index 7057c93..0000000
--- a/settings.html
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
-
- Sampler Settings
-
-
-
-
- Sampler Settings
- API Keys
- Palettes
-
-
-
-
-
-
-
diff --git a/src/file-manager.js b/src/file-manager.js
new file mode 100644
index 0000000..a483674
--- /dev/null
+++ b/src/file-manager.js
@@ -0,0 +1,275 @@
+/**
+ * File Manager - Handles all file operations and data persistence
+ */
+
+class FileManager {
+ constructor(dependencies = {}) {
+ this.appState = dependencies.appState;
+ this.updateUI = dependencies.updateUI;
+ this.updateSearchIndex = dependencies.updateSearchIndex;
+ this.updateSearchIndexForNode = dependencies.updateSearchIndexForNode;
+ this.treeNav = dependencies.treeNav;
+ this.searchManager = dependencies.searchManager;
+ this.DOM = dependencies.DOM;
+
+ // Auto-save timer
+ this.autoSaveIntervalId = null;
+ }
+
+ /**
+ * Save current loom data to file
+ */
+ async saveFile() {
+ const data = {
+ loomTree: this.appState.loomTree.serialize(),
+ focus: this.appState.focusedNode,
+ };
+ try {
+ await window.electronAPI.saveFile(data);
+ } catch (err) {
+ console.error("Save File Error:", err);
+ }
+ }
+
+ /**
+ * Load loom data from file
+ */
+ async loadFile() {
+ try {
+ const data = await window.electronAPI.loadFile();
+ if (data) {
+ await this.loadFileData(data);
+ }
+ // If data is null, user cancelled the operation
+ } catch (err) {
+ console.error("Load File Error:", err);
+ }
+ }
+
+ /**
+ * Load recent file by path
+ */
+ async loadRecentFile(filePath) {
+ try {
+ const data = await window.electronAPI.loadRecentFile(filePath);
+ if (data) {
+ await this.loadFileData(data);
+ }
+ } catch (err) {
+ console.error("Load Recent File Error:", err);
+ }
+ }
+
+ /**
+ * Load and process file data
+ */
+ async loadFileData(data) {
+ try {
+ const newLoomTree = new LoomTree();
+ newLoomTree.loadFromData(data.loomTree);
+
+ // Update global state
+ this.appState.loomTree = newLoomTree;
+
+ // Set focus to saved focus or root
+ const savedFocus =
+ data.focus && data.focus.id
+ ? newLoomTree.nodeStore[data.focus.id]
+ : newLoomTree.root;
+
+ if (!savedFocus) {
+ console.warn("Saved focus node not found, using root");
+ this.appState.focusedNode = newLoomTree.root;
+ } else {
+ this.appState.focusedNode = savedFocus;
+ }
+
+ // Ensure the focused node is properly rendered
+ if (this.appState.focusedNode) {
+ this.appState.focusedNode.cachedRenderText = newLoomTree.renderNode(
+ this.appState.focusedNode
+ );
+ }
+
+ // Update existing services with new data
+ if (this.searchManager) {
+ this.searchManager.updateLoomTree(newLoomTree);
+ this.searchManager.rebuildIndex();
+ }
+ if (this.treeNav) {
+ this.treeNav.renderTree(newLoomTree.root, this.DOM.loomTreeView);
+ }
+
+ // Render the new state
+ this.updateUI();
+
+ // Trigger an auto-save to create the temp file
+ await this.autoSave();
+ } catch (error) {
+ console.error("Error in loadFileData:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Auto-save current state
+ */
+ async autoSave() {
+ if (!this.appState.focusedNode) {
+ console.warn("Cannot auto-save: no focused node");
+ return;
+ }
+
+ const data = {
+ loomTree: this.appState.loomTree.serialize(),
+ focus: this.appState.focusedNode,
+ samplerSettingsStore: this.appState.samplerSettingsStore,
+ };
+
+ try {
+ await window.electronAPI.autoSave(data);
+ } catch (err) {
+ console.error("Auto-save Error:", err);
+ }
+ }
+
+ /**
+ * Auto-save timer tick
+ */
+ async autoSaveTick() {
+ this.appState.secondsSinceLastSave += 1;
+ if (this.appState.secondsSinceLastSave >= 30) {
+ this.autoSave();
+ this.appState.secondsSinceLastSave = 0;
+ }
+ }
+
+ /**
+ * Initialize a fresh loom state
+ */
+ initializeFreshLoom() {
+ // Create fresh loom tree
+ this.appState.loomTree = new LoomTree();
+ this.appState.focusedNode = this.appState.loomTree.root;
+
+ // Reset editor to empty state
+ this.DOM.editor.value = "";
+ this.DOM.editor.readOnly = false;
+
+ // Update existing services with fresh data
+ if (this.searchManager) {
+ this.searchManager.updateLoomTree(this.appState.loomTree);
+ this.searchManager.rebuildIndex();
+ }
+ if (this.treeNav) {
+ this.treeNav.renderTree(
+ this.appState.loomTree.root,
+ this.DOM.loomTreeView
+ );
+ }
+
+ // Update all UI components
+ this.updateUI();
+
+ // Trigger auto-save to create temp file
+ this.autoSave();
+ }
+
+ /**
+ * Initialize file manager and load initial data
+ */
+ async init() {
+ // Notify main process that renderer is ready and get any initial data (used to restore temp file after error)
+ const initialData = await window.electronAPI.rendererReady();
+ if (initialData) {
+ await this.loadFileData(initialData);
+ } else {
+ // No temp file exists, create basic fresh state
+ this.appState.loomTree = new LoomTree();
+ this.appState.focusedNode = this.appState.loomTree.root;
+ this.DOM.editor.value = "";
+ this.DOM.editor.readOnly = false;
+ }
+ }
+
+ /**
+ * Set up file-related event handlers
+ */
+ setupEventHandlers() {
+ // Set up main action handler
+ window.electronAPI.onInvokeAction((event, action, ...args) => {
+ switch (action) {
+ case "save-file":
+ this.saveFile();
+ break;
+ case "load-file":
+ this.loadFile();
+ break;
+ case "load-recent-file":
+ if (args.length > 0) {
+ this.loadRecentFile(args[0]);
+ }
+ break;
+ case "new-loom":
+ window.electronAPI.newLoom();
+ break;
+ default:
+ console.warn("FileManager: Action not recognized:", action);
+ }
+ });
+
+ window.electronAPI.onLoadInitialData(async (event, initialData) => {
+ try {
+ if (initialData && initialData.data) {
+ await this.loadFileData(initialData.data);
+ }
+ } catch (error) {
+ console.error("Error loading initial data:", error);
+ console.error("Error stack:", error.stack);
+ }
+ });
+
+ window.electronAPI.onResetToNewLoom(async event => {
+ try {
+ this.initializeFreshLoom();
+ } catch (error) {
+ console.error("Error resetting to new loom:", error);
+ console.error("Error stack:", error.stack);
+ }
+ });
+
+ // Set up final save request handler
+ window.electronAPI.onRequestFinalSave(async event => {
+ try {
+ await this.autoSave();
+ } catch (error) {
+ console.error("Error in final save:", error);
+ }
+ });
+ }
+
+ /**
+ * Start auto-save timer
+ */
+ startAutoSaveTimer() {
+ if (this.autoSaveIntervalId) {
+ clearInterval(this.autoSaveIntervalId);
+ }
+ this.autoSaveIntervalId = setInterval(() => this.autoSaveTick(), 1000);
+ }
+
+ /**
+ * Clean up timers
+ */
+ cleanup() {
+ if (this.autoSaveIntervalId) {
+ clearInterval(this.autoSaveIntervalId);
+ this.autoSaveIntervalId = null;
+ }
+ }
+}
+
+// Export for use in other modules
+if (typeof window !== "undefined") {
+ window.FileManager = FileManager;
+}
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..ec4dc3e
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,199 @@
+
+
+
+
+ MiniLoom
+
+
+
+
+
+
+
+
+
+
+
+
+
MiniHF MiniLoom
+
+
+
+ Unsaved
+
+
+ š¤ Author
+
+ š 0 nodes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ š
+ š
+
+
+
+ š 1:
+ Untitled
+
+
+ š¤ Unknown
+
+
+ š N/A
+
+
+ š 0
+
+
+ š 0
+
+
+
+
+
+
+
+
+ š
+
+ -- Select Service --
+
+
+
+ š
+
+ -- Select API key --
+
+
+
+ š²
+
+ -- Select a sampler --
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No results found
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/llm-service.js b/src/llm-service.js
new file mode 100644
index 0000000..312fb3f
--- /dev/null
+++ b/src/llm-service.js
@@ -0,0 +1,788 @@
+// HTTP request utilities
+class HTTPClient {
+ static async makeRequest(url, options) {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json; charset=UTF-8",
+ ...options.headers,
+ },
+ body: JSON.stringify(options.body),
+ });
+
+ if (!response.ok) {
+ const errorMessage = await this.extractErrorMessage(response);
+ throw new Error(`API Error: ${errorMessage}`);
+ }
+
+ try {
+ return await response.json();
+ } catch (jsonError) {
+ throw new Error(`Invalid JSON response from API: ${jsonError.message}`);
+ }
+ }
+
+ static async extractErrorMessage(response) {
+ try {
+ const errorData = await response.json();
+
+ // Handle OpenRouter provider errors with detailed metadata
+ if (errorData.error && errorData.error.metadata) {
+ const metadata = errorData.error.metadata;
+ const baseMessage =
+ errorData.error.message || "Provider returned error";
+
+ // Extract provider-specific information
+ let detailedMessage = baseMessage;
+
+ if (metadata.raw) {
+ detailedMessage += `: ${metadata.raw}`;
+ }
+
+ if (metadata.provider_name) {
+ detailedMessage += ` (Provider: ${metadata.provider_name})`;
+ }
+
+ return detailedMessage;
+ }
+
+ // Fallback to standard error message extraction
+ return errorData.error?.message || response.statusText;
+ } catch (jsonError) {
+ try {
+ const errorText = await response.text();
+ return errorText || response.statusText;
+ } catch (textError) {
+ return response.statusText;
+ }
+ }
+ }
+
+ static delay(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+}
+
+// Unified API client for all providers
+class APIClient {
+ static async callProvider(providerType, endpoint, prompt, params) {
+ switch (providerType) {
+ case "base":
+ return this.baseProvider(endpoint, prompt, params);
+ case "openai":
+ return this.openaiProvider(endpoint, prompt, params);
+ case "openai-chat":
+ return this.openaiChatProvider(endpoint, prompt, params);
+ case "openrouter":
+ return this.openrouterProvider(endpoint, prompt, params);
+ case "together":
+ return this.togetherProvider(endpoint, prompt, params);
+ case "anthropic":
+ return this.anthropicProvider(endpoint, prompt, params);
+ case "google":
+ return this.googleProvider(endpoint, prompt, params);
+ default:
+ throw new Error(`Unknown provider type: ${providerType}`);
+ }
+ }
+
+ static async baseProvider(endpoint, prompt, params) {
+ const response = await HTTPClient.makeRequest(endpoint + "generate", {
+ body: {
+ prompt: prompt,
+ prompt_node: true,
+ evaluationPrompt: "",
+ tokens_per_branch: params.tokensPerBranch,
+ output_branches: params.outputBranches,
+ },
+ });
+
+ // Base provider returns an array of responses, extract finish reasons
+ return response.map((item, index) => ({
+ text: item.text,
+ model: item.model || "base-model",
+ finish_reason: item.finish_reason || "stop", // Base provider typically stops naturally
+ }));
+ }
+
+ static async openaiProvider(endpoint, prompt, params) {
+ const headers = {
+ Authorization: `Bearer ${params.apiKey}`,
+ };
+ const requestBody = {
+ model: params.modelName,
+ prompt: prompt,
+ max_tokens: Number(params.tokensPerBranch),
+ n: Number(params.outputBranches),
+ temperature: Number(params.temperature),
+ top_p: Number(params.topP),
+ };
+
+ const response = await HTTPClient.makeRequest(endpoint, {
+ headers,
+ body: requestBody,
+ });
+
+ // Extract finish reasons from OpenAI response
+ return response.choices.map(choice => ({
+ text: choice.text,
+ model: response.model,
+ finish_reason: choice.finish_reason,
+ }));
+ }
+
+ static async openaiChatProvider(endpoint, prompt, params) {
+ const headers = {
+ Authorization: `Bearer ${params.apiKey}`,
+ };
+ const requestBody = {
+ messages: [{ role: "system", content: prompt }],
+ model: params.modelName,
+ max_completion_tokens: params.tokensPerBranch,
+ temperature: params.temperature,
+ top_p: params.topP,
+ };
+
+ const response = await HTTPClient.makeRequest(endpoint, {
+ headers,
+ body: requestBody,
+ });
+
+ // Extract finish reasons from OpenAI Chat response
+ return response.choices.map(choice => ({
+ text: choice.message.content,
+ model: response.model,
+ finish_reason: choice.finish_reason,
+ }));
+ }
+
+ static async openrouterProvider(endpoint, prompt, params) {
+ const authToken = `Bearer ${params.apiKey}`;
+ const apiDelay = Number(params.delay || 0);
+
+ const batchPromises = [];
+ const calls = params.outputBranches;
+
+ for (let i = 1; i <= calls; i++) {
+ const body = {
+ model: params.modelName,
+ prompt: prompt,
+ max_tokens: Number(params.tokensPerBranch),
+ n: 1,
+ temperature: Number(params.temperature),
+ top_p: Number(params.topP),
+ top_k: Number(params.topK),
+ repetition_penalty: Number(params.repetitionPenalty),
+ provider: { require_parameters: true },
+ };
+
+ const promise = HTTPClient.delay(apiDelay * i)
+ .then(() => {
+ const headers = {
+ accept: "application/json",
+ Authorization: authToken,
+ "HTTP-Referer": "https://github.com/JD-P/miniloom",
+ "X-Title": "MiniLoom",
+ };
+
+ return HTTPClient.makeRequest(endpoint, {
+ headers,
+ body,
+ });
+ })
+ .then(responseJson => {
+ return responseJson.choices.map(choice => ({
+ text: choice.text,
+ model: responseJson.model,
+ finish_reason: choice.finish_reason,
+ }));
+ });
+
+ batchPromises.push(promise);
+ }
+
+ const batch = await Promise.all(batchPromises);
+ return batch.flat();
+ }
+
+ static async togetherProvider(endpoint, prompt, params) {
+ const authToken = `Bearer ${params.apiKey}`;
+ const apiDelay = Number(params.delay || 0);
+
+ const batchPromises = [];
+ const calls = params.outputBranches;
+
+ for (let i = 1; i <= calls; i++) {
+ const body = {
+ model: params.modelName,
+ prompt: prompt,
+ max_tokens: Number(params.tokensPerBranch),
+ n: 1,
+ temperature: Number(params.temperature),
+ top_p: Number(params.topP),
+ top_k: Number(params.topK),
+ repetition_penalty: Number(params.repetitionPenalty),
+ };
+
+ const promise = HTTPClient.delay(apiDelay * i)
+ .then(() => {
+ const headers = {
+ accept: "application/json",
+ Authorization: authToken,
+ };
+
+ return HTTPClient.makeRequest(endpoint, {
+ headers,
+ body,
+ });
+ })
+ .then(responseJson => {
+ return responseJson.output.choices.map(choice => ({
+ text: choice.text,
+ model: responseJson.model,
+ finish_reason: choice.finish_reason,
+ }));
+ });
+
+ batchPromises.push(promise);
+ }
+
+ const batch = await Promise.all(batchPromises);
+ return batch.flat();
+ }
+
+ static async anthropicProvider(endpoint, prompt, params) {
+ const headers = {
+ "Content-Type": "application/json",
+ "x-api-key": params.apiKey,
+ "anthropic-version": "2023-06-01",
+ };
+ const apiDelay = Number(params.delay || 0);
+
+ const batchPromises = [];
+ const calls = params.outputBranches;
+
+ for (let i = 1; i <= calls; i++) {
+ const requestBody = {
+ model: params.modelName,
+ messages: [{ role: "user", content: prompt }],
+ max_tokens: Number(params.tokensPerBranch),
+ temperature: Number(params.temperature),
+ top_p: Number(params.topP),
+ };
+
+ const promise = HTTPClient.delay(apiDelay * i)
+ .then(() => {
+ return HTTPClient.makeRequest(endpoint, {
+ headers,
+ body: requestBody,
+ });
+ })
+ .then(response => {
+ return [
+ {
+ text: response.content[0].text,
+ model: response.model,
+ finish_reason: response.stop_reason,
+ },
+ ];
+ });
+
+ batchPromises.push(promise);
+ }
+
+ const batch = await Promise.all(batchPromises);
+ return batch.flat();
+ }
+
+ static async googleProvider(endpoint, prompt, params) {
+ const headers = {
+ "Content-Type": "application/json",
+ "X-goog-api-key": params.apiKey,
+ };
+ const apiDelay = Number(params.delay || 0);
+
+ const batchPromises = [];
+ const calls = params.outputBranches;
+
+ for (let i = 1; i <= calls; i++) {
+ const requestBody = {
+ contents: [
+ {
+ parts: [
+ {
+ text: prompt,
+ },
+ ],
+ },
+ ],
+ generationConfig: {
+ maxOutputTokens: Number(params.tokensPerBranch),
+ temperature: Number(params.temperature),
+ topP: Number(params.topP),
+ topK: Number(params.topK),
+ },
+ };
+
+ const promise = HTTPClient.delay(apiDelay * i)
+ .then(() => {
+ return HTTPClient.makeRequest(endpoint, {
+ headers,
+ body: requestBody,
+ });
+ })
+ .then(response => {
+ return response.candidates.map(candidate => ({
+ text: candidate.content.parts[0].text,
+ model: response.model || params.modelName,
+ finish_reason: candidate.finishReason || "stop",
+ }));
+ });
+
+ batchPromises.push(promise);
+ }
+
+ const batch = await Promise.all(batchPromises);
+ return batch.flat();
+ }
+}
+
+// Main LLM Service class
+class LLMService {
+ constructor(dependencies = {}) {
+ this.settingsProvider = dependencies.settingsProvider;
+ this.dataProvider = dependencies.dataProvider;
+ this.eventHandlers = dependencies.eventHandlers || {};
+ }
+
+ // Configuration and parameter management
+ prepareGenerationParams() {
+ const settings = this.settingsProvider.getSamplerSettings();
+ const samplerSettingsStore =
+ this.settingsProvider.getSamplerSettingsStore();
+
+ if (!samplerSettingsStore) {
+ throw new Error("Sampler settings store not available");
+ }
+
+ const serviceData =
+ samplerSettingsStore.services?.[settings.selectedServiceName] || {};
+ const samplerData =
+ samplerSettingsStore.samplers?.[settings.selectedSamplerName] || {};
+ const apiKey =
+ samplerSettingsStore["api-keys"]?.[settings.selectedApiKeyName] || "";
+
+ return {
+ // Service parameters
+ samplingMethod: serviceData["sampling-method"] || "base",
+ apiUrl: serviceData["service-api-url"] || "",
+ modelName: serviceData["service-model-name"] || "",
+ apiDelay: parseInt(serviceData["service-api-delay"]) || 3000,
+ apiKey: apiKey,
+
+ // Sampler parameters
+ outputBranches: parseInt(samplerData["output-branches"]) || 2,
+ tokensPerBranch: parseInt(samplerData["tokens-per-branch"]) || 256,
+ temperature: parseFloat(samplerData["temperature"]) || 0.9,
+ topP: parseFloat(samplerData["top-p"]) || 1,
+ topK: parseInt(samplerData["top-k"]) || 100,
+ repetitionPenalty: parseFloat(samplerData["repetition-penalty"]) || 1,
+ };
+ }
+
+ // Core generation methods
+ async generateResponses(
+ endpoint,
+ {
+ prompt,
+ weave = true,
+ weaveParams = {},
+ focusId = null,
+ includePrompt = false,
+ }
+ ) {
+ if (focusId) {
+ const loomTree = this.dataProvider.getLoomTree();
+ loomTree.renderNode(loomTree.nodeStore[focusId]);
+ }
+
+ const finalEndpoint = endpoint + (weave ? "weave" : "generate");
+ const params = {
+ prompt: prompt,
+ prompt_node: includePrompt,
+ tokens_per_branch: weaveParams.tokens_per_branch,
+ output_branches: weaveParams.output_branches,
+ };
+
+ return HTTPClient.makeRequest(finalEndpoint, { body: params });
+ }
+
+ async generateSummary(taskText) {
+ if (!taskText || typeof taskText !== "string") {
+ console.warn("generateSummary called with invalid taskText:", taskText);
+ return "Summary Not Available";
+ }
+
+ const params = this.prepareGenerationParams();
+ const endpoint = params.apiUrl;
+
+ const summarizePromptTemplate =
+ await window.electronAPI.readPromptFile("summarize.txt");
+ const summarizePrompt = summarizePromptTemplate.replace(
+ "{MODEL_NAME}",
+ params.modelName
+ );
+
+ // Limit context to 8 * 512 characters (average word length * word count)
+ const prompt =
+ summarizePrompt +
+ "\n\n\n" +
+ taskText.slice(-4096) +
+ "\n \n\nThree Words:";
+
+ const samplingMethod = params.samplingMethod;
+
+ // Use unified provider call for summary generation
+ if (
+ [
+ "base",
+ "openai",
+ "openai-chat",
+ "openrouter",
+ "together",
+ "anthropic",
+ "google",
+ ].includes(samplingMethod)
+ ) {
+ const summaryParams = {
+ ...params,
+ tokensPerBranch: 10,
+ outputBranches: 1,
+ };
+
+ try {
+ const response = await APIClient.callProvider(
+ samplingMethod,
+ endpoint,
+ prompt,
+ summaryParams
+ );
+
+ return window.utils.extractThreeWords(response[0].text);
+ } catch (error) {
+ console.warn("Summary generation failed:", error);
+ return "Summary Failed";
+ }
+ } else {
+ return "Summary Not Available";
+ }
+ }
+
+ // Main generation entry point - now directly maps to provider methods
+ async generateNewResponses(nodeId) {
+ const params = this.prepareGenerationParams();
+ const samplingMethod = params.samplingMethod;
+
+ // Direct method mapping - names now match settings dropdown exactly
+ const methodMap = {
+ base: () => this.generateWithProvider(nodeId, "base"),
+ openai: () => this.generateWithProvider(nodeId, "openai"),
+ "openai-chat": () => this.generateWithOpenAIChat(nodeId),
+ openrouter: () => this.generateWithProvider(nodeId, "openrouter"),
+ together: () => this.generateWithProvider(nodeId, "together"),
+ anthropic: () => this.generateWithProvider(nodeId, "anthropic"),
+ google: () => this.generateWithProvider(nodeId, "google"),
+ };
+
+ const method = methodMap[samplingMethod] || methodMap["base"];
+ await method();
+ }
+
+ // Unified generation method for most providers
+ async generateWithProvider(nodeId, providerType) {
+ await this.executeGeneration(nodeId, async () => {
+ const params = this.prepareGenerationParams();
+ const prompt = this.dataProvider.getCurrentPrompt();
+
+ const providerParams = {
+ apiKey: params.apiKey,
+ modelName: params.modelName,
+ outputBranches: params.outputBranches,
+ tokensPerBranch: params.tokensPerBranch,
+ temperature: params.temperature,
+ topP: params.topP,
+ topK: params.topK,
+ repetitionPenalty: params.repetitionPenalty,
+ delay: params.apiDelay,
+ };
+
+ const newResponses = await APIClient.callProvider(
+ providerType,
+ params.apiUrl,
+ prompt,
+ providerParams
+ );
+
+ return newResponses;
+ });
+ }
+
+ async generateWithOpenAIChat(nodeId) {
+ await this.executeGeneration(nodeId, async () => {
+ const params = this.prepareGenerationParams();
+ const loomTree = this.dataProvider.getLoomTree();
+ const rollFocus = loomTree.nodeStore[nodeId];
+ const promptText = loomTree.renderNode(rollFocus);
+
+ const chatData = this.parseChatData(promptText);
+ const requestBody = this.buildChatRequestBody(chatData, params);
+ const headers = this.buildChatRequestHeaders(params);
+
+ const responseData = await HTTPClient.makeRequest(params.apiUrl, {
+ headers,
+ body: requestBody,
+ });
+
+ await this.processChatResponses(
+ responseData,
+ chatData,
+ rollFocus,
+ this.getLastChildIndex(rollFocus)
+ );
+
+ return []; // Chat responses are processed separately
+ });
+ }
+
+ // Common generation execution pattern
+ async executeGeneration(nodeId, generationFunction) {
+ if (this.eventHandlers.onGenerationStarted) {
+ this.eventHandlers.onGenerationStarted(nodeId);
+ }
+
+ try {
+ if (this.eventHandlers.onPreGeneration) {
+ await this.eventHandlers.onPreGeneration(nodeId);
+ }
+ const rollFocus = this.dataProvider.getLoomTree().nodeStore[nodeId];
+ const lastChildIndex = this.getLastChildIndex(rollFocus);
+
+ const newResponses = await generationFunction();
+
+ if (newResponses.length > 0) {
+ await this.processResponses(
+ newResponses,
+ rollFocus,
+ lastChildIndex,
+ this.prepareGenerationParams().apiDelay
+ );
+ }
+
+ if (this.eventHandlers.onGenerationCompleted) {
+ this.eventHandlers.onGenerationCompleted(nodeId, newResponses);
+ }
+ } catch (error) {
+ if (this.eventHandlers.onGenerationFailed) {
+ this.eventHandlers.onGenerationFailed(nodeId, error.message);
+ }
+ throw error;
+ } finally {
+ if (this.eventHandlers.onGenerationFinished) {
+ this.eventHandlers.onGenerationFinished(nodeId);
+ }
+ }
+ }
+
+ // Response processing
+ async processResponses(newResponses, rollFocus, lastChildIndex, apiDelay) {
+ const loomTree = this.dataProvider.getLoomTree();
+
+ if (!Array.isArray(newResponses) || newResponses.length === 0) {
+ console.warn(
+ "processResponses called with invalid responses:",
+ newResponses
+ );
+ return;
+ }
+
+ loomTree.clearNodeError(rollFocus.id);
+
+ for (const response of newResponses) {
+ if (!response || typeof response.text !== "string") {
+ console.warn("Invalid response object:", response);
+ continue;
+ }
+
+ // Check if no tokens were generated and model finished
+ const hasNoContent = !response.text || response.text.trim() === "";
+ const isFinished =
+ response.finish_reason === "stop" ||
+ response.finish_reason === "end_turn" ||
+ response.finish_reason === "assistant";
+
+ let responseSummary;
+ if (hasNoContent && isFinished) {
+ responseSummary = "Text Complete";
+ } else {
+ responseSummary = await HTTPClient.delay(apiDelay).then(() => {
+ return this.generateSummary(response.text);
+ });
+ }
+
+ const childText = loomTree.renderNode(rollFocus) + response.text;
+ const responseNode = loomTree.createNode(
+ "gen",
+ rollFocus,
+ childText,
+ responseSummary
+ );
+
+ // Notify that a new node was created
+ if (this.eventHandlers.onNodeCreated) {
+ this.eventHandlers.onNodeCreated(responseNode.id, {
+ node: responseNode,
+ metadata: {
+ model: response.model,
+ finishReason: response.finish_reason,
+ },
+ fullText: loomTree.renderNode(responseNode),
+ });
+ }
+ }
+
+ // Notify that tree view should be updated
+ if (this.eventHandlers.onTreeViewUpdate) {
+ this.eventHandlers.onTreeViewUpdate();
+ }
+
+ this.updateFocus(rollFocus, lastChildIndex);
+ }
+
+ async processChatResponses(
+ responseData,
+ chatData,
+ rollFocus,
+ lastChildIndex
+ ) {
+ const loomTree = this.dataProvider.getLoomTree();
+
+ loomTree.clearNodeError(rollFocus.id);
+
+ for (const choice of responseData.choices) {
+ const assistantMessage = choice.message;
+ const newChatData = JSON.parse(JSON.stringify(chatData));
+ newChatData.messages.push({
+ role: assistantMessage.role,
+ content: assistantMessage.content,
+ });
+
+ const newChatText = JSON.stringify(newChatData, null, 2);
+
+ // Check if no tokens were generated and model finished
+ const hasNoContent =
+ !assistantMessage.content || assistantMessage.content.trim() === "";
+ const isFinished =
+ choice.finish_reason === "stop" ||
+ choice.finish_reason === "end_turn" ||
+ choice.finish_reason === "assistant";
+
+ let summary;
+ if (hasNoContent && isFinished) {
+ summary = "Text Complete";
+ } else {
+ summary = await this.generateSummary(
+ assistantMessage.content || "Assistant response"
+ );
+ }
+
+ const responseNode = loomTree.createNode(
+ "gen",
+ rollFocus,
+ newChatText,
+ summary
+ );
+
+ // Notify that a new node was created
+ if (this.eventHandlers.onNodeCreated) {
+ this.eventHandlers.onNodeCreated(responseNode.id, {
+ node: responseNode,
+ metadata: {
+ model: responseData.model,
+ usage: responseData.usage,
+ finishReason: choice.finish_reason,
+ },
+ fullText: loomTree.renderNode(responseNode),
+ });
+ }
+ }
+
+ // Notify that tree view should be updated
+ if (this.eventHandlers.onTreeViewUpdate) {
+ this.eventHandlers.onTreeViewUpdate();
+ }
+
+ this.updateFocus(rollFocus, lastChildIndex);
+ }
+
+ // Chat-specific utilities
+ parseChatData(promptText) {
+ try {
+ const chatData = JSON.parse(promptText);
+ if (!chatData.messages || !Array.isArray(chatData.messages)) {
+ throw new Error("Invalid chat format: messages array not found");
+ }
+ return chatData;
+ } catch (jsonError) {
+ return {
+ messages: [{ role: "user", content: promptText.trim() }],
+ };
+ }
+ }
+
+ buildChatRequestBody(chatData, params) {
+ return {
+ model: params.modelName,
+ messages: chatData.messages,
+ max_completion_tokens: parseInt(params.tokensPerBranch),
+ temperature: parseFloat(params.temperature),
+ top_p: parseFloat(params.topP),
+ n: parseInt(params.outputBranches),
+ };
+ }
+
+ buildChatRequestHeaders(params) {
+ return {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${params.apiKey}`,
+ };
+ }
+
+ // Utility methods
+ getLastChildIndex(rollFocus) {
+ return rollFocus.children.length > 0 ? rollFocus.children.length - 1 : null;
+ }
+
+ updateFocus(rollFocus, lastChildIndex) {
+ const currentFocus = this.dataProvider.getCurrentFocus();
+ const loomTree = this.dataProvider.getLoomTree();
+
+ if (
+ currentFocus === rollFocus &&
+ this.eventHandlers.onFocusChanged &&
+ loomTree
+ ) {
+ let targetNodeId;
+ if (lastChildIndex === null) {
+ targetNodeId = rollFocus.children[0];
+ } else {
+ targetNodeId = rollFocus.children[lastChildIndex + 1];
+ }
+
+ if (targetNodeId) {
+ this.eventHandlers.onFocusChanged(targetNodeId, "llm-generation");
+ }
+ }
+ }
+}
+
+window.LLMService = LLMService;
diff --git a/src/main.css b/src/main.css
new file mode 100644
index 0000000..aa4fd04
--- /dev/null
+++ b/src/main.css
@@ -0,0 +1,1893 @@
+/* ==========================================================================
+ CSS Variables (Color Palette & Design Tokens)
+ ========================================================================== */
+
+:root {
+ /* Primary Color Palette */
+ --barn-red: #750504ff;
+ --black-olive: #293121ff;
+ --oxford-blue: #142b4fff;
+ --night: #0d0e10ff;
+ --engineering-orange: #d50402ff;
+
+ /* Extended Color Palette */
+ --primary: var(--engineering-orange);
+ --primary-hover: var(--barn-red);
+ --secondary: var(--oxford-blue);
+ --accent: var(--black-olive);
+ --text-primary: var(--night);
+ --text-secondary: #666;
+ --text-muted: #999;
+ --text-light: #777;
+
+ /* Gradient Colors */
+ --gradient-primary: #667eea;
+ --gradient-secondary: #764ba2;
+
+ /* Additional Colors */
+ --error-bg-light: #fff5f5;
+ --unread-indicator: #7bb3f0;
+ --search-hover: #f0f0f0;
+ --search-highlight: #e0e0e0;
+
+ /* Background Colors */
+ --bg-primary: #ffffff;
+ --bg-secondary: #f8f9fa;
+ --bg-tertiary: #e9ecef;
+ --bg-hover: rgba(0, 0, 0, 0.02);
+ --bg-active: rgba(213, 4, 2, 0.1);
+ --bg-focus: rgba(213, 4, 2, 0.04);
+
+ /* Border Colors */
+ --border-primary: #e9ecef;
+ --border-secondary: #ddd;
+ --border-hover: #adb5bd;
+ --border-focus: #007bff;
+ --border-error: #dc3545;
+ --border-success: #28a745;
+
+ /* Status Colors */
+ --success: #28a745;
+ --success-bg: #d4edda;
+ --success-border: #c3e6cb;
+ --error: #dc3545;
+ --error-bg: #f8d7da;
+ --error-border: #f5c6cb;
+ --warning: #ffc107;
+ --info: #007bff;
+
+ /* Interactive Colors */
+ --button-primary: var(--engineering-orange);
+ --button-primary-hover: var(--barn-red);
+ --button-secondary: #007bff;
+ --button-secondary-hover: #0056b3;
+ --button-success: var(--success);
+ --button-success-hover: #1e7e34;
+
+ /* Shadow Colors */
+ --shadow-light: rgba(0, 0, 0, 0.1);
+ --shadow-medium: rgba(0, 0, 0, 0.15);
+ --shadow-heavy: rgba(0, 0, 0, 0.4);
+ --shadow-focus: rgba(0, 123, 255, 0.25);
+ --shadow-primary: rgba(213, 4, 2, 0.2);
+
+ /* Typography */
+ --font-family-primary:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ --font-family-mono:
+ ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
+ --font-size-xs: 11px;
+ --font-size-sm: 12px;
+ --font-size-base: 14px;
+ --font-size-lg: 16px;
+ --font-size-xl: 18px;
+ --font-size-2xl: 24px;
+ --font-weight-normal: 400;
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+ --line-height-tight: 1.2;
+ --line-height-normal: 1.4;
+ --line-height-relaxed: 1.5;
+
+ /* Spacing */
+ --spacing-xs: 4px;
+ --spacing-sm: 8px;
+ --spacing-md: 12px;
+ --spacing-lg: 16px;
+ --spacing-xl: 20px;
+ --spacing-2xl: 24px;
+ --spacing-3xl: 32px;
+
+ /* Border Radius */
+ --radius-sm: 3px;
+ --radius-md: 4px;
+ --radius-lg: 6px;
+ --radius-xl: 8px;
+ --radius-2xl: 12px;
+ --radius-full: 50%;
+
+ /* Transitions */
+ --transition-fast: 0.15s ease;
+ --transition-normal: 0.2s ease;
+ --transition-slow: 0.3s ease;
+
+ /* Z-Index Scale */
+ --z-dropdown: 1000;
+ --z-modal: 1050;
+ --z-tooltip: 1100;
+}
+
+/* ==========================================================================
+ Base Styles & Reset
+ ========================================================================== */
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--font-family-primary);
+ font-size: var(--font-size-base);
+ line-height: var(--line-height-normal);
+ color: var(--text-primary);
+ margin: 0;
+ padding: 0;
+ background-color: var(--bg-primary);
+ display: flex;
+ height: 100vh;
+ min-width: 800px;
+ overflow-x: auto;
+ overflow-y: auto;
+ width: 100%;
+}
+
+/* ==========================================================================
+ Typography
+ ========================================================================== */
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 0 0 var(--spacing-sm) 0;
+ font-weight: var(--font-weight-semibold);
+ line-height: var(--line-height-tight);
+}
+
+h1 {
+ font-size: var(--font-size-2xl);
+}
+h2 {
+ font-size: var(--font-size-xl);
+}
+h3 {
+ font-size: var(--font-size-lg);
+}
+h4,
+h5,
+h6 {
+ font-size: var(--font-size-base);
+}
+
+p {
+ margin: 0 0 var(--spacing-sm) 0;
+}
+
+.mono {
+ font-family: var(--font-family-mono);
+}
+
+.muted {
+ opacity: 0.7;
+ font-size: 0.9em;
+ color: var(--text-muted);
+}
+
+/* ==========================================================================
+ Form Elements
+ ========================================================================== */
+
+input,
+select,
+textarea {
+ font-family: inherit;
+ font-size: var(--font-size-base);
+ line-height: var(--line-height-normal);
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ border: 1px solid var(--border-secondary);
+ border-radius: var(--radius-md);
+ padding: var(--spacing-sm) var(--spacing-md);
+ transition: all var(--transition-normal);
+ outline: none;
+}
+
+input:focus,
+select:focus,
+textarea:focus {
+ border-color: var(--border-focus);
+ box-shadow: 0 0 0 2px var(--shadow-focus);
+}
+
+input:hover,
+select:hover,
+textarea:hover {
+ border-color: #adb5bd;
+}
+
+input::placeholder,
+textarea::placeholder {
+ color: var(--text-muted);
+}
+
+select {
+ cursor: pointer;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
+ background-position: right var(--spacing-sm) center;
+ background-repeat: no-repeat;
+ background-size: 16px 12px;
+ padding-right: var(--spacing-2xl);
+ appearance: none;
+}
+
+/* ==========================================================================
+ Layout & Structure
+ ========================================================================== */
+
+/* Main Application Layout */
+.main-content {
+ display: flex;
+ flex: 1;
+ min-width: 600px;
+ overflow: hidden;
+}
+
+.narrative-pane {
+ flex: 1;
+ min-width: 400px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ height: 100vh;
+}
+
+.nav-pane {
+ width: 25%;
+ min-width: 150px;
+ max-width: 350px;
+ background-color: var(--bg-secondary);
+ border-left: 1px solid var(--border-primary);
+ overflow: hidden;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+/* Top Bar Layout */
+.top-bar {
+ border-bottom: 1px solid var(--border-primary);
+ padding: var(--spacing-sm) var(--spacing-lg);
+ height: 56px;
+ display: flex;
+ align-items: center;
+ background-color: var(--bg-primary);
+}
+
+.top-bar-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+}
+
+.logo-title {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: var(--spacing-sm);
+}
+
+.app-logo {
+ width: 48px;
+ height: 48px;
+ object-fit: contain;
+}
+
+.app-title {
+ font-weight: var(--font-weight-semibold);
+ font-size: 1.4em;
+ color: var(--text-primary);
+ line-height: 1;
+}
+
+.author-info {
+ font-size: var(--font-size-sm);
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.file-info {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: var(--spacing-xs);
+ font-size: var(--font-size-base);
+ color: var(--text-secondary);
+}
+
+.tree-stats {
+ font-size: var(--font-size-sm);
+ color: var(--text-light);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+ position: relative;
+}
+
+.tree-stats-summary {
+ cursor: pointer;
+ padding: 2px 4px;
+ border-radius: 3px;
+ transition: background-color 0.2s;
+ position: relative;
+}
+
+.tree-stats-summary:hover {
+ background-color: var(--bg-secondary);
+}
+
+.file-title-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.current-filename {
+ font-weight: var(--font-weight-medium);
+}
+
+/* Editor Layout */
+#focused-node-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.editor {
+ border: 1px solid #e0e0e0;
+ border-bottom: none;
+ outline: none;
+ flex: 1;
+ width: 100%;
+ height: 100%;
+ overflow-y: auto;
+ padding: 16px;
+ resize: none;
+ font-family: inherit;
+ font-size: 14px;
+ line-height: 1.5;
+ min-height: 0;
+ max-height: none;
+ border-radius: 4px 4px 0 0;
+}
+
+.editor-footer {
+ background-color: #e8e8e8;
+ border: 1px solid #e0e0e0;
+ border-top: none;
+ padding: 4px 12px;
+ font-size: 11px;
+ color: #555;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 0 0 4px 4px;
+}
+
+.editor-stats {
+ font-family: monospace;
+ font-size: 11px;
+ margin-top: 2px;
+ margin-bottom: 0;
+}
+
+/* Controls Layout */
+.controls {
+ background-color: var(--bg-primary);
+ border-top: 1px solid var(--border-primary);
+ padding: var(--spacing-sm) var(--spacing-lg);
+ min-height: 48px;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: space-between;
+ flex-shrink: 0;
+ gap: var(--spacing-md);
+ position: relative;
+}
+
+.controls-left {
+ display: flex;
+ flex-direction: row;
+ gap: var(--spacing-md);
+ flex: 1;
+ min-width: 0;
+ align-items: flex-start;
+}
+
+.controls-right {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: var(--spacing-sm);
+ flex-shrink: 0;
+ margin-right: 0;
+}
+
+/* Form Field Styles */
+.form-field {
+ margin-bottom: var(--spacing-lg);
+}
+
+.form-field label {
+ display: block;
+ margin-bottom: var(--spacing-sm);
+ font-weight: var(--font-weight-medium);
+ color: var(--text-primary);
+ font-size: var(--font-size-base);
+ min-width: 100px;
+}
+
+.form-field input,
+.form-field select {
+ width: 100%;
+ padding: var(--spacing-md);
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: var(--radius-lg);
+ font-size: var(--font-size-base);
+ transition: all var(--transition-normal);
+ box-sizing: border-box;
+}
+
+.form-field input:focus,
+.form-field select:focus {
+ outline: none;
+ border-color: var(--engineering-orange);
+ box-shadow: 0 0 0 2px rgba(213, 4, 2, 0.2);
+}
+
+.form-field input::placeholder {
+ color: var(--text-muted);
+}
+
+.form-row {
+ display: flex;
+ gap: var(--spacing-lg);
+ margin-bottom: var(--spacing-lg);
+}
+
+.form-row .form-field {
+ flex: 1;
+ margin-bottom: 0;
+}
+
+.form-row label {
+ display: block;
+ margin-bottom: var(--spacing-sm);
+ font-weight: var(--font-weight-medium);
+ font-size: var(--font-size-base);
+ color: var(--text-primary);
+}
+
+.form-row input,
+.form-row select {
+ width: 100%;
+ min-width: 350px;
+ padding: var(--spacing-md);
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: var(--radius-lg);
+ font-size: var(--font-size-base);
+ transition: all var(--transition-normal);
+ box-sizing: border-box;
+}
+
+.form-row input:focus,
+.form-row select:focus {
+ outline: none;
+ border-color: var(--engineering-orange);
+ box-shadow: 0 0 0 2px rgba(213, 4, 2, 0.2);
+}
+
+.form-actions {
+ display: flex;
+ gap: var(--spacing-sm);
+ margin-top: var(--spacing-lg);
+ padding-top: var(--spacing-md);
+ border-top: 1px solid var(--border-primary);
+}
+
+.form-actions .btn {
+ min-width: 80px;
+}
+
+/* ==========================================================================
+ Table Styles
+ ========================================================================== */
+
+table.keys {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: var(--spacing-sm);
+ font-family: inherit;
+ font-size: var(--font-size-base);
+}
+
+table.keys th,
+table.keys td {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+ padding: var(--spacing-md) var(--spacing-sm);
+ text-align: left;
+ font-family: inherit;
+}
+
+table.keys th {
+ font-weight: var(--font-weight-semibold);
+ color: var(--text-primary);
+ background-color: var(--bg-secondary);
+}
+
+table.keys td {
+ color: var(--text-primary);
+}
+
+/* Force wrapping for secret column */
+table.keys td:nth-child(2) {
+ max-width: 300px;
+ word-wrap: break-word;
+ word-break: break-all;
+ overflow-wrap: break-word;
+}
+
+/* ==========================================================================
+ Footer and Message Styles
+ ========================================================================== */
+
+.footer {
+ height: 40px;
+ background-color: var(--bg-secondary);
+ border-top: 1px solid var(--border-primary);
+ display: flex;
+ align-items: center;
+ padding: 0 var(--spacing-lg);
+ font-size: var(--font-size-sm);
+ color: var(--text-secondary);
+}
+
+.footer.success {
+ color: var(--success);
+ background-color: var(--success-bg);
+ border-top-color: var(--success-border);
+}
+
+.footer.error {
+ color: var(--error);
+ background-color: var(--error-bg);
+ border-top-color: var(--error-border);
+}
+
+/* Fixed footer for notifications */
+#footer {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: var(--spacing-sm) var(--spacing-lg);
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-medium);
+ text-align: center;
+ z-index: var(--z-modal);
+ transition: all var(--transition-slow);
+ opacity: 0;
+ transform: translateY(100%);
+ pointer-events: none;
+}
+
+#footer.success {
+ background-color: var(--success-bg);
+ color: var(--success);
+ border-top: 1px solid var(--success-border);
+ opacity: 1;
+ transform: translateY(0);
+}
+
+#footer.error {
+ background-color: var(--error-bg);
+ color: var(--error);
+ border-top: 1px solid var(--error-border);
+ opacity: 1;
+ transform: translateY(0);
+}
+
+#footer:not(:empty) {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+/* ==========================================================================
+ Buttons & Interactive Elements
+ ========================================================================== */
+
+.btn {
+ padding: 6px 12px;
+ border: 1px solid var(--border-secondary);
+ border-radius: var(--radius-md);
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ font-size: var(--font-size-sm);
+ cursor: pointer;
+ transition: all var(--transition-normal);
+ font-weight: var(--font-weight-medium);
+}
+
+.btn:hover {
+ background-color: var(--bg-secondary);
+ border-color: var(--border-hover);
+}
+
+.btn.primary {
+ background-color: var(--button-secondary);
+ border-color: var(--button-secondary);
+ color: white;
+}
+
+.btn.primary:hover {
+ background-color: var(--button-secondary-hover);
+ border-color: var(--button-secondary-hover);
+}
+
+/* Special Button Styles */
+.reroll {
+ background-color: var(--engineering-orange);
+ color: white;
+ border: none;
+ border-radius: 12px;
+ padding: 0 12px;
+ font-size: 1.3em;
+ font-weight: normal;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ transition: background-color 0.2s;
+ height: 40px;
+ white-space: nowrap;
+}
+
+.reroll:hover {
+ background-color: var(--barn-red);
+}
+
+.reroll span {
+ font-size: 0.8em;
+}
+
+.thumbs {
+ font-size: 1.3em;
+ cursor: pointer;
+ height: 42px;
+ width: 42px;
+ border-radius: 8px;
+ background-color: #f8f9fa;
+ color: #6c757d;
+ border: 2px solid #dee2e6;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.8;
+ transition: all 0.2s;
+ margin-right: 0;
+}
+
+.thumbs:hover {
+ opacity: 1;
+ background-color: #e9ecef;
+ border-color: #adb5bd;
+}
+
+.thumbs.chosen {
+ background-color: #f8f9fa !important;
+ opacity: 1 !important;
+ border-width: 3px !important;
+}
+
+.thumbs.chosen.thumbs-up {
+ background-color: rgba(40, 167, 69, 0.3) !important;
+ border-color: #28a745 !important;
+ color: #28a745 !important;
+}
+
+.thumbs.chosen.thumbs-up:hover {
+ background-color: rgba(40, 167, 69, 0.4) !important;
+ border-color: #28a745 !important;
+ color: #28a745 !important;
+}
+
+.thumbs.chosen.thumbs-down {
+ background-color: rgba(220, 53, 69, 0.3) !important;
+ border-color: #dc3545 !important;
+ color: #dc3545 !important;
+}
+
+.thumbs.chosen.thumbs-down:hover {
+ background-color: rgba(220, 53, 69, 0.4) !important;
+ border-color: #dc3545 !important;
+ color: #dc3545 !important;
+}
+
+/* Thumbs display container */
+.thumbs-display {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin-right: 6px;
+ align-items: center;
+}
+
+/* ==========================================================================
+ Utility Classes
+ ========================================================================== */
+
+.row {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-lg);
+}
+
+.row label {
+ font-weight: var(--font-weight-medium);
+ color: var(--text-primary);
+ min-width: 90px;
+ font-size: var(--font-size-base);
+}
+
+.divider {
+ height: 1px;
+ background-color: var(--border-primary);
+ margin: var(--spacing-xs) 0 var(--spacing-md) 0;
+}
+
+.help-icon {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ background-color: var(--text-secondary);
+ color: white;
+ border-radius: var(--radius-full);
+ text-align: center;
+ line-height: 16px;
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-bold);
+ cursor: help;
+ margin-left: var(--spacing-sm);
+ flex-shrink: 0;
+ transition: none;
+}
+
+.help-icon:hover {
+ background-color: var(--text-primary);
+}
+
+.close-btn {
+ position: absolute;
+ top: var(--spacing-md);
+ right: var(--spacing-md);
+ background: var(--bg-primary);
+ border: none;
+ font-size: var(--font-size-2xl);
+ cursor: pointer;
+ color: var(--text-secondary);
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-full);
+ transition: all var(--transition-normal);
+ font-weight: 300;
+ z-index: 10;
+ box-shadow: 0 2px 8px var(--shadow-medium);
+}
+
+.close-btn:hover {
+ background-color: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.close-btn:active {
+ background: #d0d0d0;
+}
+
+/* ==========================================================================
+ Animations
+ ========================================================================== */
+
+@keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* ==========================================================================
+ Tree Navigation Status Indicators
+ ========================================================================== */
+
+.node-status {
+ display: inline-block;
+ margin-left: -1px;
+ margin-right: 2px;
+ font-size: 12px;
+ line-height: 1;
+ transition: all var(--transition-normal);
+}
+
+.node-status.status-error {
+ color: var(--error);
+ animation: pulse 2s infinite;
+}
+
+.node-status.status-pending {
+ color: var(--warning);
+ animation: spin 0.6s linear infinite;
+}
+
+.node-status.status-unread {
+ color: var(--unread-indicator);
+ font-weight: normal;
+}
+
+.node-status.status-normal {
+ color: transparent;
+}
+
+/* ==========================================================================
+ Node Rating Indicators
+ ========================================================================== */
+
+.node-rating {
+ display: inline-block;
+ margin-right: 4px;
+ font-size: 12px;
+ line-height: 1;
+ transition: all var(--transition-normal);
+}
+
+.node-rating.rating-star {
+ color: #ffd700;
+ font-weight: var(--font-weight-medium);
+}
+
+.node-rating.rating-no {
+ color: var(--error);
+ font-weight: var(--font-weight-medium);
+}
+
+.node-rating.rating-none {
+ color: transparent;
+}
+
+/* ==========================================================================
+ Hidden Parents Indicator
+ ========================================================================== */
+
+.hidden-parents-indicator {
+ display: block;
+ margin: 4px 0;
+ padding: 6px 8px;
+ background-color: rgba(213, 4, 2, 0.05);
+ border: 1px solid rgba(213, 4, 2, 0.15);
+ border-radius: 6px;
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-weight: var(--font-weight-medium);
+ transition: all var(--transition-normal);
+ border-left: 3px solid rgba(213, 4, 2, 0.3);
+}
+
+.hidden-parents-indicator:hover {
+ background-color: rgba(213, 4, 2, 0.08);
+ border-color: rgba(213, 4, 2, 0.25);
+ transform: translateX(1px);
+}
+
+.hidden-parents-link {
+ color: var(--text-secondary);
+ text-decoration: none;
+ cursor: pointer;
+ transition: all var(--transition-normal);
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.hidden-parents-link:hover {
+ color: var(--primary);
+ text-decoration: none;
+}
+
+/* ==========================================================================
+ Compressed Children Indicator
+ ========================================================================== */
+
+.compressed-children-indicator {
+ display: block;
+ margin: 0;
+ padding: 2px 6px;
+ background-color: transparent;
+ border: none;
+ border-radius: 3px;
+ font-size: 11px;
+ color: var(--text-muted);
+ font-style: italic;
+ opacity: 0.8;
+ transition: all var(--transition-normal);
+}
+
+.compressed-children-indicator:hover {
+ opacity: 1;
+}
+
+.compressed-children-link {
+ color: var(--text-muted);
+ text-decoration: none;
+ cursor: pointer;
+ transition: color var(--transition-normal);
+}
+
+.compressed-children-link:hover {
+ color: var(--primary);
+ text-decoration: underline;
+}
+
+/* Remove blue styling from compressed children when parent is downvoted */
+.downvoted .compressed-children-indicator {
+ background-color: transparent;
+ border: none;
+ border-left: none;
+}
+
+.downvoted .compressed-children-link {
+ color: var(--text-muted);
+}
+
+.downvoted .compressed-children-link:hover {
+ color: var(--text-muted);
+ text-decoration: none;
+}
+
+/* Tooltip */
+.tooltip {
+ position: fixed;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-primary);
+ border-radius: 4px;
+ padding: 8px;
+ font-size: 12px;
+ white-space: pre-line;
+ z-index: 99999;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ color: var(--text-primary);
+ text-align: left;
+ cursor: text;
+ user-select: text;
+ min-width: 200px;
+ pointer-events: none;
+}
+
+/* ==========================================================================
+ Node Display & Statistics
+ ========================================================================== */
+
+.node-stats-display {
+ font-size: var(--font-size-base);
+ color: var(--text-secondary);
+ margin-top: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.subtree-info {
+ cursor: pointer;
+ position: relative;
+}
+
+.subtree-info:hover::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--bg-primary);
+ border: 1px solid var(--border-primary);
+ border-radius: 4px;
+ padding: 8px;
+ font-size: 12px;
+ white-space: pre-line;
+ z-index: 1000;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ color: var(--text-primary);
+ min-width: 120px;
+ text-align: left;
+ margin-bottom: 4px;
+}
+
+.node-info-line {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ font-size: 14px;
+ flex-wrap: wrap;
+ padding: 0px 0;
+}
+
+.node-info-line.metadata-line {
+ font-size: 12px;
+ color: var(--text-light);
+ padding: 1px 0;
+}
+
+.node-info-line.metadata-line span {
+ padding: 0px 1px;
+ font-size: 12px;
+ margin-right: 6px;
+}
+
+.node-info-line.metadata-line span:last-child {
+ margin-right: 0;
+}
+
+.node-info-line span {
+ padding: 1px 2px;
+ border-radius: 3px;
+ transition: background-color 0.2s;
+ font-size: 14px;
+ line-height: 1.4;
+ vertical-align: baseline;
+}
+
+.finish-reason {
+ color: var(--text-secondary);
+ font-size: var(--font-size-sm);
+ margin-right: 2px !important;
+}
+
+#node-summary {
+ font-weight: var(--font-weight-semibold);
+ color: var(--text-primary);
+}
+
+/* Thumbs for focused node display */
+.node-stats-display .thumbs {
+ font-size: 1.3em;
+ height: 40px;
+ width: 40px;
+ border-radius: 8px;
+ background-color: #007bff;
+ color: white;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.7;
+ transition: all 0.2s;
+ margin-left: 8px;
+ margin-right: 4px;
+ cursor: pointer;
+}
+
+.node-stats-display .thumbs:hover {
+ opacity: 1;
+ background-color: #0056b3;
+ transform: scale(1.05);
+}
+
+.node-stats-display .thumbs:active {
+ transform: scale(0.95);
+ background-color: #004085;
+}
+
+.node-stats-display .thumbs.chosen {
+ background-color: #0056b3 !important;
+ opacity: 1 !important;
+}
+
+.editor-stats span {
+ padding: 1px 4px;
+ border-radius: 3px;
+ margin: 0;
+}
+
+.editor-stats .positive {
+ color: #28a745;
+ font-weight: var(--font-weight-medium);
+}
+
+.editor-stats .negative {
+ color: #dc3545;
+ font-weight: var(--font-weight-medium);
+}
+
+/* Search result rating indicators */
+.search-result-rating-positive {
+ color: #28a745;
+ font-size: 1.1em;
+}
+
+.search-result-rating-negative {
+ color: #dc3545;
+ font-size: 1.1em;
+}
+
+/* ==========================================================================
+ Generation Controls & Favorites
+ ========================================================================== */
+
+#thumbs-display {
+ display: flex;
+ gap: var(--spacing-xs);
+ align-items: flex-start;
+ flex-shrink: 0;
+}
+
+#generation-controls {
+ display: flex;
+ gap: var(--spacing-md);
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.control-group {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+}
+
+.control-group label {
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-medium);
+ color: var(--text-secondary);
+ margin: 0;
+ cursor: help;
+ flex-shrink: 0;
+ transition: all var(--transition-normal);
+ position: relative;
+}
+
+.control-group label:hover {
+ transform: scale(1.1);
+ filter: brightness(1.2);
+ text-shadow: 0 0 8px rgba(0, 123, 255, 0.3);
+}
+
+.control-group label:active {
+ transform: scale(0.95);
+}
+
+.control-group label::after {
+ content: "āļø";
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ font-size: 8px;
+ opacity: 0;
+ transition: opacity var(--transition-normal);
+}
+
+.control-group label:hover::after {
+ opacity: 1;
+}
+
+.control-group select {
+ padding: 6px 10px;
+ border: 1px solid var(--border-secondary);
+ border-radius: 4px;
+ background-color: var(--bg-primary);
+ font-size: 14px;
+ color: var(--text-primary);
+ cursor: pointer;
+ min-width: 100px;
+ max-width: 150px;
+ transition: border-color 0.2s;
+}
+
+.control-group select:focus {
+ outline: none;
+ border-color: var(--button-secondary);
+ box-shadow: 0 0 0 2px var(--shadow-focus);
+}
+
+.control-group select:hover {
+ border-color: var(--border-hover);
+}
+
+.generate-button-container {
+ display: inline-flex;
+ align-items: center;
+ flex-shrink: 0;
+ position: relative;
+}
+
+.generate-button-container .reroll {
+ width: auto !important;
+ max-width: none !important;
+ flex: none !important;
+}
+
+.generate-section {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+.favorites-container {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.favorites-row {
+ display: flex;
+ gap: 4px;
+}
+
+.favorite-btn {
+ padding: 4px 8px;
+ border: 1px solid var(--border-secondary);
+ border-radius: 4px;
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ font-size: 11px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 40px;
+ text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.favorite-btn:hover {
+ background-color: var(--bg-secondary);
+ border-color: var(--border-hover);
+}
+
+.favorite-btn:active {
+ background-color: var(--button-secondary);
+ color: white;
+}
+
+.favorite-btn.empty {
+ opacity: 0.3;
+ cursor: default;
+}
+
+.favorite-btn.empty:hover {
+ background-color: var(--bg-primary);
+ border-color: var(--border-secondary);
+}
+
+.die {
+ height: 24px;
+ width: 24px;
+ font-size: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transform-origin: center center;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s ease;
+ position: relative;
+ z-index: 10;
+ margin-right: 8px;
+ flex-shrink: 0;
+}
+
+.die.rolling {
+ animation: spin 0.6s linear infinite;
+ opacity: 1;
+ visibility: visible;
+}
+
+/* ==========================================================================
+ Error Handling & Messages
+ ========================================================================== */
+
+.errors {
+ margin: 0;
+ padding: 0;
+ height: 0;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ border-top: none;
+ background-color: var(--error-bg-light);
+ position: relative;
+ z-index: 10;
+}
+
+.errors.has-error {
+ margin: 0;
+ padding: 12px 16px;
+ height: auto;
+ box-shadow: 0 2px 4px rgba(220, 53, 69, 0.1);
+ background-color: #ffebee;
+ border: none;
+ position: relative;
+ z-index: 1000;
+ display: block !important;
+ overflow: visible;
+}
+
+.error-content {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.error-message {
+ color: var(--error);
+ font-size: 14px;
+ font-weight: 500;
+ margin: 0;
+ line-height: 1.4;
+ display: block;
+ min-height: 20px;
+ visibility: visible;
+ opacity: 1;
+ height: auto;
+ overflow: visible;
+ white-space: normal;
+ word-wrap: break-word;
+ position: static;
+ transform: none;
+ flex: 1;
+}
+
+.error-close {
+ background: none;
+ border: none;
+ color: var(--error);
+ font-size: 18px;
+ font-weight: bold;
+ cursor: pointer;
+ padding: 0;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.error-close:hover {
+ background-color: rgba(220, 53, 69, 0.1);
+ color: #b02a37;
+}
+
+.error-close:active {
+ background-color: rgba(220, 53, 69, 0.2);
+}
+
+/* ==========================================================================
+ Tree Navigation & Search
+ ========================================================================== */
+
+/* Tree View */
+.loom-tree-view {
+ flex: 1;
+ margin: var(--spacing-lg);
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ font-size: 14px;
+ line-height: 1.3;
+ min-height: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.loom-tree-view ul {
+ margin-top: 0;
+ padding-left: 0.6em;
+ margin-left: 0em;
+ list-style: none;
+ position: relative;
+ width: 100%;
+ min-width: 0;
+}
+
+.loom-tree-view > ul {
+ padding-left: 0.2em;
+ margin-top: 0;
+}
+
+.loom-tree-view ul::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: rgba(41, 49, 33, 0.2);
+}
+
+.loom-tree-view > ul::before {
+ content: none;
+}
+
+/* Properly stop the vertical connector at the last child */
+.loom-tree-view li:last-child::after {
+ content: "";
+ position: absolute;
+ left: -0.6em; /* align with ul::before */
+ top: 0.9em; /* start just below the elbow */
+ bottom: 0; /* mask to bottom */
+ width: 1px;
+ background: var(--bg-secondary); /* match nav background to hide tail */
+ z-index: 2;
+}
+
+.loom-tree-view li::before {
+ content: "";
+ position: absolute;
+ left: -0.6em;
+ top: 0.8em;
+ width: 0.6em;
+ height: 1px;
+ background: rgba(41, 49, 33, 0.2);
+ z-index: 1;
+}
+
+.loom-tree-view li {
+ position: relative;
+ margin: 1px 0;
+ width: 100%;
+ min-width: 0;
+}
+
+.loom-tree-view li a {
+ min-height: 1.2em;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 1px;
+ padding: 2px 4px 2px 8px;
+ border-radius: var(--radius-md);
+ text-decoration: none;
+ color: var(--text-primary);
+ background: transparent;
+ transition: all var(--transition-normal);
+ position: relative;
+ vertical-align: middle;
+ flex-wrap: nowrap;
+ box-sizing: border-box;
+}
+
+/* Status and rating spans - fixed width, no stretching */
+.loom-tree-view li a .node-status,
+.loom-tree-view li a .node-rating,
+.loom-tree-view li a .author-indicator {
+ flex-shrink: 0;
+ flex-grow: 0;
+ width: auto;
+ min-width: 0;
+}
+
+/* Control order of elements: status first (leftmost), then author, then text, then ratings */
+.loom-tree-view li a .node-status {
+ order: -3; /* Leftmost */
+}
+
+.loom-tree-view li a .author-indicator {
+ order: -2; /* Second */
+}
+
+.loom-tree-view li a .node-rating {
+ order: 1; /* Last */
+}
+
+/* Hide empty spans to prevent spacing */
+.loom-tree-view li a .node-status:empty,
+.loom-tree-view li a .node-rating:empty,
+.loom-tree-view li a .author-indicator:empty {
+ display: none;
+}
+
+/* Ensure non-empty spans are visible */
+.loom-tree-view li a .node-status:not(:empty),
+.loom-tree-view li a .node-rating:not(:empty),
+.loom-tree-view li a .author-indicator:not(:empty) {
+ display: inline-block;
+}
+
+/* Text span - flexible, truncates with ellipsis */
+.loom-tree-view
+ li
+ a
+ span:not(.node-status):not(.node-rating):not(.author-indicator) {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-right: 0; /* No right margin needed with new layout */
+ margin-left: 0; /* No left margin to ensure it's close to badges */
+ display: block; /* Ensure proper text truncation */
+ max-width: 100%; /* Ensure it doesn't overflow */
+ order: 0; /* Main content in the middle */
+ opacity: 0.9; /* Slightly less than 1.0 opacity for all summaries */
+}
+
+/* Rating - fixed size, positioned to the right */
+.loom-tree-view li a .node-rating {
+ flex-shrink: 0;
+ flex-grow: 0;
+ width: auto;
+ min-width: 12px;
+ max-width: none;
+ margin-left: auto; /* Push to the right */
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 4px;
+ font-size: 11px;
+ line-height: 1;
+ text-align: center;
+}
+
+/* Rating colors - no backgrounds, just colored text */
+.loom-tree-view li a .node-rating.rating-star {
+ background-color: transparent;
+ border: none;
+ color: #28a745;
+ transform: translate(1px, 5px); /* Moved down 2px more */
+}
+
+.loom-tree-view li a .node-rating.rating-no {
+ background-color: transparent;
+ border: none;
+ color: #dc3545;
+ transform: translate(1px, 5px); /* Moved down 2px more */
+}
+
+.loom-tree-view li a .node-rating.rating-none {
+ background-color: transparent;
+ border: none;
+ color: transparent;
+ transform: none;
+}
+
+.loom-tree-view li a:hover {
+ cursor: pointer;
+ background: var(--bg-focus);
+ transform: translateX(1px);
+}
+
+/* Prevent hover effects on focused and parent nodes */
+.loom-tree-view #focused-node > a:hover,
+.loom-tree-view .parent-of-focused > a:hover {
+ transform: none;
+}
+
+.loom-tree-view .parent-of-focused > a:hover {
+ background: rgba(213, 4, 2, 0.08);
+ color: var(--text-primary);
+ border-left: 3px solid #dc3545;
+}
+
+.downvoted,
+.downvoted * {
+ opacity: 0.85;
+}
+
+.upvoted span:not(.node-status):not(.node-rating):not(.author-indicator) {
+ opacity: 1; /* Full opacity for thumbs up nodes */
+}
+
+.loom-tree-view #focused-node > a {
+ background: var(--bg-active);
+ font-weight: var(--font-weight-semibold) !important;
+ border-left: 4px solid var(--primary);
+}
+
+.loom-tree-view #focused-node > a:hover {
+ transform: none;
+ background: var(--bg-active);
+ border-left: 4px solid var(--primary);
+}
+
+.loom-tree-view li a .unread-node {
+ font-weight: normal;
+ font-style: normal;
+}
+
+.loom-tree-view li a .status-unread {
+ opacity: 0.8;
+}
+
+.loom-tree-view .parent-of-focused > a {
+ background: rgba(213, 4, 2, 0.03);
+ font-weight: var(--font-weight-medium);
+ color: var(--text-primary);
+ border-left: 3px solid #dc3545;
+ border-radius: 4px;
+ padding: 6px 10px;
+ font-size: 13px;
+}
+
+.subtree-badge {
+ background-color: #4a5568;
+ color: white;
+ padding: 1px 4px;
+ border-radius: 8px;
+ font-size: 10px;
+ font-weight: 500;
+ display: inline-block;
+ min-width: 12px;
+ text-align: center;
+ line-height: 1.2;
+ flex-shrink: 0;
+ flex-grow: 0;
+ width: auto;
+ max-width: none;
+ box-sizing: border-box;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.subtree-badge-unread {
+ background-color: #dc3545;
+}
+
+/* Combined status-badge styling */
+.node-status.status-badge {
+ background-color: #4a5568;
+ color: white;
+ padding: 1px 4px;
+ border-radius: 8px;
+ font-size: 10px;
+ font-weight: 500;
+ display: inline-block;
+ min-width: 12px;
+ text-align: center;
+ line-height: 1.2;
+ margin-right: 1px;
+ flex-shrink: 0;
+ flex-grow: 0;
+ width: auto;
+ max-width: none;
+ margin-left: 0;
+ box-sizing: border-box;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.node-status.status-badge-unread {
+ background-color: #dc3545;
+}
+
+.root-node-link {
+ font-weight: bold;
+ color: #007bff;
+ font-size: 1.1em;
+}
+
+.author-indicator {
+ color: #007bff;
+ font-size: 0.9em;
+ margin-right: 1px;
+}
+
+/* Styling for expanded parent nodes */
+.expanded-parent-node {
+ margin: 2px 0;
+ padding: 0;
+ background: rgba(213, 4, 2, 0.03);
+ border-radius: 4px;
+ transition: all var(--transition-normal);
+ list-style: none; /* Remove list styling */
+ margin-left: 0;
+}
+
+.expanded-parent-node:hover {
+ background: rgba(213, 4, 2, 0.06);
+}
+
+.expanded-parents-container {
+ margin: 6px 0;
+ padding: 6px 6px 6px 0;
+ list-style: none;
+}
+
+.expanded-parent-node a {
+ padding: 6px 10px !important;
+ font-size: 13px;
+ color: var(--text-secondary);
+ font-weight: var(--font-weight-medium);
+}
+
+.expanded-parent-node a:hover {
+ background: rgba(213, 4, 2, 0.08) !important;
+ transform: none !important;
+ color: var(--text-primary);
+}
+
+.hidden-parents-divider {
+ margin: 8px 0;
+ border: none;
+ border-top: 1px solid rgba(41, 49, 33, 0.2);
+}
+
+/* Search Components */
+.search-container {
+ margin: 0;
+ padding: 0;
+ position: sticky;
+ top: 0;
+ width: 100%;
+ flex-shrink: 0;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-primary);
+ height: 56px;
+ display: flex;
+ align-items: center;
+ z-index: 5;
+}
+
+.search-input-wrapper {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ padding: var(--spacing-sm) var(--spacing-lg);
+ display: flex;
+ align-items: center;
+}
+
+.search-input {
+ width: 100%;
+ padding: 8px 12px;
+ padding-right: 40px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ font-size: 14px;
+ text-align: left !important;
+ transition: all 0.2s ease;
+ box-sizing: border-box;
+ background: var(--bg-primary);
+}
+
+.search-clear {
+ position: absolute;
+ right: 16px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ font-size: 18px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 3px;
+ transition: all 0.2s ease;
+ line-height: 1;
+}
+
+.search-clear:hover {
+ color: var(--text-primary);
+ background-color: var(--bg-secondary);
+}
+
+.search-input:focus {
+ outline: none;
+ border-color: var(--engineering-orange);
+ box-shadow: 0 0 0 2px rgba(213, 4, 2, 0.2);
+}
+
+.search-input:hover {
+ border-color: var(--border-hover);
+}
+
+.search-result-item {
+ border: 1px solid var(--border-secondary);
+ margin: 5px 0;
+ padding: 8px;
+ cursor: pointer;
+ border-radius: 3px;
+ background: var(--bg-primary);
+ transition: background-color 0.2s;
+}
+
+.search-result-item:hover {
+ background-color: var(--search-hover);
+}
+
+.search-result-header {
+ font-weight: var(--font-weight-bold);
+ color: var(--text-primary);
+ margin-bottom: var(--spacing-xs);
+}
+
+.search-result-content {
+ font-size: 0.9em;
+ color: var(--text-secondary);
+ line-height: 1.3;
+}
+
+.search-result-meta {
+ font-size: 0.8em;
+ color: var(--text-muted);
+ margin-top: var(--spacing-xs);
+}
+
+.search-result-date {
+ margin-top: 2px;
+ color: var(--text-muted);
+}
+
+.search-no-results {
+ padding: var(--spacing-md);
+ color: var(--text-secondary);
+ font-style: italic;
+ margin-top: var(--spacing-md);
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 0;
+}
+
+.search-results-container {
+ flex: 1;
+ overflow-y: auto;
+ margin-top: var(--spacing-md);
+ min-height: 0;
+}
+
+/* Highlight styling for search results */
+mark {
+ background-color: yellow;
+ font-weight: var(--font-weight-bold);
+ padding: 1px 2px;
+}
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..6f4f34b
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,813 @@
+const {
+ app,
+ BrowserWindow,
+ ipcMain,
+ dialog,
+ Menu,
+ MenuItem,
+} = require("electron");
+const fs = require("fs");
+const path = require("path");
+
+let mainWindow = null;
+let tempFilePath = null;
+let currentFilePath = null;
+let recentFiles = [];
+let lastSavedTimestamp = null;
+const MAX_RECENT_FILES = 5;
+
+function setCurrentFile(filePath, isTemp = false) {
+ currentFilePath = filePath;
+ if (!isTemp && filePath !== tempFilePath) {
+ addToRecentFiles(filePath);
+ }
+}
+
+function getFileStats(filePath) {
+ try {
+ const stats = fs.statSync(filePath);
+ return {
+ mtime: stats.mtime,
+ birthtime: stats.birthtime,
+ };
+ } catch (error) {
+ console.error("Error getting file stats:", error);
+ return { mtime: new Date(), birthtime: new Date() };
+ }
+}
+
+function hasContent(data) {
+ return (
+ data.loomTree &&
+ data.loomTree.nodeStore &&
+ Object.keys(data.loomTree.nodeStore).length > 1
+ );
+}
+
+async function checkUnsavedChanges() {
+ if (mainWindow) {
+ mainWindow.webContents.send("request-final-save");
+ // Give the renderer a moment to process the save
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ // Only check for unsaved changes if we're working with a temp file
+ if (currentFilePath === tempFilePath && fs.existsSync(tempFilePath)) {
+ try {
+ const content = fs.readFileSync(tempFilePath, "utf8");
+ const data = JSON.parse(content);
+
+ if (hasContent(data)) {
+ const result = await dialog.showMessageBox(mainWindow, {
+ type: "question",
+ buttons: ["Discard", "Save As...", "Cancel"],
+ defaultId: 1,
+ title: "Unsaved Changes",
+ message: "You have unsaved changes. What would you like to do?",
+ detail:
+ "Choose 'Save As...' to save your work, 'Discard' to lose your changes, or 'Cancel' to keep working.",
+ });
+
+ if (result.response === 0) {
+ // Discard - delete temp file
+ if (fs.existsSync(tempFilePath)) {
+ fs.unlinkSync(tempFilePath);
+ }
+ return true;
+ } else if (result.response === 1) {
+ // Save As...
+ const { filePath } = await dialog.showSaveDialog(mainWindow, {
+ title: "Save Loom As",
+ filters: [{ name: "JSON Files", extensions: ["json"] }],
+ });
+
+ if (filePath) {
+ fs.writeFileSync(filePath, JSON.stringify(data));
+ addToRecentFiles(filePath);
+ // Delete temp file after successful save
+ if (fs.existsSync(tempFilePath)) {
+ fs.unlinkSync(tempFilePath);
+ }
+ return true;
+ } else {
+ // User cancelled save dialog
+ return false;
+ }
+ } else {
+ // Cancel
+ return false;
+ }
+ }
+ } catch (error) {
+ console.error("Error checking for unsaved changes:", error);
+ }
+ }
+ return true;
+}
+
+// Trigger final auto-save before changing file context
+async function finalAutoSave() {
+ if (mainWindow && currentFilePath && currentFilePath !== tempFilePath) {
+ // Request the renderer to send current data for final save
+ mainWindow.webContents.send("request-final-save");
+ }
+}
+
+function createWindow(initialData = null) {
+ try {
+ const window = new BrowserWindow({
+ title: "MiniLoom",
+ icon: path.join(__dirname, "..", "assets/minihf_logo_no_text.png"),
+ width: 1200,
+ height: 900,
+ show: false, // Don't show until ready
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, "preload.js"),
+ webSecurity: true,
+ allowRunningInsecureContent: false,
+ sandbox: false,
+ devTools: true, // Enable dev tools for debugging
+ },
+ });
+ mainWindow = window;
+
+ loadRecentFiles();
+ const existingMenuTemplate = Menu.getApplicationMenu().items.map(item => {
+ return {
+ label: item.label,
+ submenu: item.submenu.items,
+ };
+ });
+ // Update the File menu
+ updateFileMenu(existingMenuTemplate);
+
+ const editMenuItems = [
+ {
+ label: "LLM Settings",
+ accelerator: "CmdOrCtrl+P",
+ click: () => openSettingsWindow(),
+ },
+ { type: "separator" },
+ {
+ label: "Undo",
+ accelerator: "CmdOrCtrl+Z",
+ role: "undo",
+ },
+ {
+ label: "Redo",
+ accelerator: "CmdOrCtrl+Shift+Z",
+ role: "redo",
+ },
+ { type: "separator" },
+ {
+ label: "Cut",
+ accelerator: "CmdOrCtrl+X",
+ role: "cut",
+ },
+ {
+ label: "Copy",
+ accelerator: "CmdOrCtrl+C",
+ role: "copy",
+ },
+ {
+ label: "Paste",
+ accelerator: "CmdOrCtrl+V",
+ role: "paste",
+ },
+ {
+ label: "Select All",
+ accelerator: "CmdOrCtrl+A",
+ role: "selectAll",
+ },
+ { type: "separator" },
+ ];
+ const editMenuIndex = existingMenuTemplate.findIndex(
+ item => item.label === "Edit"
+ );
+ if (editMenuIndex >= 0) {
+ existingMenuTemplate[editMenuIndex].submenu = [
+ ...editMenuItems,
+ { type: "separator" },
+ ...existingMenuTemplate[editMenuIndex].submenu,
+ ];
+ }
+
+ // Add Help menu
+ const helpMenuItems = [
+ {
+ label: "Get Help (Discord)",
+ click: () => {
+ require("electron").shell.openExternal(
+ "https://discord.gg/Y3HGwrcPwr"
+ );
+ },
+ },
+ {
+ label: "Report Issues (GitHub)",
+ click: () => {
+ require("electron").shell.openExternal(
+ "https://github.com/JD-P/miniloom"
+ );
+ },
+ },
+ ];
+
+ // Check if Help menu already exists
+ const helpMenuIndex = existingMenuTemplate.findIndex(
+ item => item.label === "Help"
+ );
+ if (helpMenuIndex >= 0) {
+ // Replace existing Help menu
+ existingMenuTemplate[helpMenuIndex] = {
+ label: "Help",
+ submenu: helpMenuItems,
+ };
+ } else {
+ // Add new Help menu at the end
+ existingMenuTemplate.push({
+ label: "Help",
+ submenu: helpMenuItems,
+ });
+ }
+
+ // Build and set the new menu
+ const newMenu = Menu.buildFromTemplate(existingMenuTemplate);
+ Menu.setApplicationMenu(newMenu);
+
+ try {
+ mainWindow.loadFile("src/index.html");
+
+ // Add error handler for webContents
+ mainWindow.webContents.on(
+ "did-fail-load",
+ (event, errorCode, errorDescription, validatedURL) => {
+ console.error(
+ "Failed to load:",
+ errorCode,
+ errorDescription,
+ validatedURL
+ );
+ }
+ );
+
+ mainWindow.webContents.on("crashed", event => {
+ console.error("WebContents crashed");
+ });
+
+ mainWindow.once("ready-to-show", () => {
+ mainWindow.show();
+ });
+
+ mainWindow.webContents.on("console-message", event => {
+ const { level, message, line, sourceId } = event;
+ console.log(`Renderer [${level}]: ${message} (${sourceId}:${line})`);
+ });
+
+ mainWindow.on("unresponsive", () => {
+ console.error("Window became unresponsive");
+ });
+ } catch (error) {
+ console.error("Error loading index.html:", error);
+ throw error;
+ }
+
+ initializeTempFile();
+
+ window.on("closed", function () {
+ if (window === mainWindow) {
+ mainWindow = null;
+ }
+ });
+
+ return window;
+ } catch (error) {
+ console.error("Error in createWindow:", error);
+ return null;
+ }
+}
+
+function initializeTempFile() {
+ const appDataPath = app.getPath("appData");
+ const miniLoomDir = path.join(appDataPath, "miniloom");
+ const tempDir = path.join(miniLoomDir, "temp");
+
+ if (!fs.existsSync(miniLoomDir)) {
+ fs.mkdirSync(miniLoomDir);
+ }
+ if (!fs.existsSync(tempDir)) {
+ fs.mkdirSync(tempDir);
+ }
+
+ tempFilePath = path.join(tempDir, "temp_tree.json");
+ currentFilePath = tempFilePath;
+}
+
+function openSettingsWindow(tabName = null) {
+ const modal = new BrowserWindow({
+ parent: mainWindow,
+ modal: true,
+ show: false,
+ width: 900,
+ height: 700,
+ minWidth: 600,
+ minHeight: 400,
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, "preload.js"),
+ webSecurity: true,
+ allowRunningInsecureContent: false,
+ sandbox: false,
+ },
+ });
+
+ modal.loadFile("src/settings-modal.html");
+ modal.once("ready-to-show", () => {
+ modal.show();
+ // Send the tab name to the settings window if provided
+ if (tabName && typeof tabName === "string") {
+ modal.webContents.send("open-to-tab", tabName);
+ }
+ });
+
+ modal.on("closed", () => {
+ // Inform the main window to refresh its UI
+ mainWindow.webContents.send("settings-updated");
+ });
+}
+
+// Listen for open-settings request
+ipcMain.handle("open-settings", () => openSettingsWindow());
+
+// Listen for open-settings-to-tab request
+ipcMain.handle("open-settings-to-tab", (event, tabName) =>
+ openSettingsWindow(tabName)
+);
+
+// Listen for close-settings-window request
+ipcMain.on("close-settings-window", event => {
+ const sender = event.sender;
+ const window = BrowserWindow.fromWebContents(sender);
+ if (window) {
+ window.close();
+ }
+});
+
+ipcMain.handle("read-prompt-file", async (event, filename) => {
+ const promptPath = path.join(__dirname, "..", "prompts", filename);
+ try {
+ return fs.readFileSync(promptPath, "utf8");
+ } catch (error) {
+ throw new Error(`Failed to read prompt file: ${error.message}`);
+ }
+});
+
+// Load recent files from settings
+function loadRecentFiles() {
+ try {
+ const appDataPath = app.getPath("appData");
+ const miniLoomDir = path.join(appDataPath, "miniloom");
+ const recentFilesPath = path.join(miniLoomDir, "recent_files.json");
+
+ if (fs.existsSync(recentFilesPath)) {
+ const content = fs.readFileSync(recentFilesPath, "utf8");
+ recentFiles = JSON.parse(content);
+ // Filter out files that no longer exist
+ recentFiles = recentFiles.filter(filePath => fs.existsSync(filePath));
+ }
+ } catch (error) {
+ console.error("Error loading recent files:", error);
+ recentFiles = [];
+ }
+}
+
+// Save recent files to settings
+function saveRecentFiles() {
+ try {
+ const appDataPath = app.getPath("appData");
+ const miniLoomDir = path.join(appDataPath, "miniloom");
+ const recentFilesPath = path.join(miniLoomDir, "recent_files.json");
+
+ if (!fs.existsSync(miniLoomDir)) {
+ fs.mkdirSync(miniLoomDir);
+ }
+
+ fs.writeFileSync(recentFilesPath, JSON.stringify(recentFiles));
+ } catch (error) {
+ console.error("Error saving recent files:", error);
+ }
+}
+
+// Add file to recent files list
+function addToRecentFiles(filePath) {
+ if (!filePath || filePath === tempFilePath) return;
+
+ // Remove if already exists
+ recentFiles = recentFiles.filter(path => path !== filePath);
+
+ // Add to beginning
+ recentFiles.unshift(filePath);
+
+ // Keep only the most recent files
+ if (recentFiles.length > MAX_RECENT_FILES) {
+ recentFiles = recentFiles.slice(0, MAX_RECENT_FILES);
+ }
+
+ saveRecentFiles();
+ updateMenu();
+}
+
+// Update menu with recent files
+function buildFileMenuItems() {
+ const fileMenuItems = [
+ {
+ label: "New",
+ accelerator: "CmdOrCtrl+N",
+ click() {
+ mainWindow.webContents.send("invoke-action", "new-loom");
+ },
+ },
+ {
+ label: "Open",
+ accelerator: "CmdOrCtrl+O",
+ click() {
+ mainWindow.webContents.send("invoke-action", "load-file");
+ },
+ },
+ {
+ label: "Save As",
+ accelerator: "CmdOrCtrl+S",
+ click() {
+ mainWindow.webContents.send("invoke-action", "save-file");
+ },
+ },
+ { type: "separator" },
+ ];
+
+ // Add recent files to menu
+ if (recentFiles.length > 0) {
+ recentFiles.forEach((filePath, index) => {
+ const fileName = path.basename(filePath, ".json");
+ fileMenuItems.push({
+ label: `${index + 1}. ${fileName}`,
+ click() {
+ mainWindow.webContents.send(
+ "invoke-action",
+ "load-recent-file",
+ filePath
+ );
+ },
+ });
+ });
+ fileMenuItems.push({ type: "separator" });
+ }
+
+ fileMenuItems.push({
+ label: "Close App",
+ accelerator: "CmdOrCtrl+Q",
+ click: async () => {
+ const canQuit = await checkUnsavedChanges();
+
+ if (canQuit) {
+ app.quit();
+ }
+ },
+ });
+
+ return fileMenuItems;
+}
+
+function updateFileMenu(existingMenuTemplate) {
+ const fileMenuItems = buildFileMenuItems();
+
+ const fileMenuIndex = existingMenuTemplate.findIndex(
+ item => item.label === "File"
+ );
+
+ if (fileMenuIndex >= 0) {
+ existingMenuTemplate[fileMenuIndex] = {
+ label: "File",
+ submenu: fileMenuItems,
+ };
+ } else {
+ // If File menu doesn't exist, add it at the beginning
+ existingMenuTemplate.unshift({
+ label: "File",
+ submenu: fileMenuItems,
+ });
+ }
+
+ return existingMenuTemplate;
+}
+
+function updateMenu() {
+ if (!mainWindow) return;
+
+ const existingMenuTemplate = Menu.getApplicationMenu().items.map(item => {
+ return {
+ label: item.label,
+ submenu: item.submenu.items,
+ };
+ });
+
+ const updatedTemplate = updateFileMenu(existingMenuTemplate);
+ const newMenu = Menu.buildFromTemplate(updatedTemplate);
+ Menu.setApplicationMenu(newMenu);
+}
+
+// File operation handlers
+ipcMain.handle("save-file", async (event, data) => {
+ // Always show save dialog to save as a new file
+ const { filePath } = await dialog.showSaveDialog(mainWindow, {
+ title: "Save As",
+ filters: [{ name: "JSON Files", extensions: ["json"] }],
+ });
+
+ if (filePath) {
+ fs.writeFileSync(filePath, JSON.stringify(data));
+ lastSavedTimestamp = new Date();
+
+ // Only delete temp file if we're saving over the temp file itself
+ if (currentFilePath === tempFilePath && filePath === tempFilePath) {
+ // This shouldn't happen with the new save-as behavior, but keep for safety
+ } else if (currentFilePath === tempFilePath && filePath !== tempFilePath) {
+ // When saving temp file to a new location, delete the temp file
+ try {
+ if (fs.existsSync(tempFilePath)) {
+ fs.unlinkSync(tempFilePath);
+ }
+ } catch (error) {
+ console.error("Error deleting temp file:", error);
+ }
+ }
+ // If saving from a regular file to a new file, leave the original file unchanged
+
+ setCurrentFile(filePath);
+ mainWindow.webContents.send(
+ "update-filename",
+ path.basename(filePath),
+ new Date(),
+ filePath,
+ false,
+ lastSavedTimestamp
+ );
+ }
+});
+
+ipcMain.handle("new-loom", async event => {
+ // Check for unsaved changes first
+ if (await checkUnsavedChanges()) {
+ // Reset to temp file for new loom
+ setCurrentFile(tempFilePath, true);
+ lastSavedTimestamp = null; // No last saved time for new loom
+ mainWindow.webContents.send(
+ "update-filename",
+ "Unsaved",
+ null,
+ tempFilePath,
+ true,
+ lastSavedTimestamp
+ ); // isTemp = true
+
+ // Send a simple signal to reset the UI
+ mainWindow.webContents.send("reset-to-new-loom");
+ }
+});
+
+ipcMain.handle("load-file", async event => {
+ await finalAutoSave();
+
+ // Check for unsaved changes first
+ if (await checkUnsavedChanges()) {
+ const { filePaths } = await dialog.showOpenDialog(mainWindow, {
+ title: "Open",
+ filters: [{ name: "JSON Files", extensions: ["json"] }],
+ properties: ["openFile"],
+ });
+
+ if (filePaths && filePaths.length > 0) {
+ const filePath = filePaths[0];
+ const content = fs.readFileSync(filePath, "utf8");
+ const data = JSON.parse(content);
+
+ // Load the file in the current window
+ const stats = getFileStats(filePath);
+ lastSavedTimestamp = stats.mtime; // Use file's modification time as last saved
+ setCurrentFile(filePath, false);
+ mainWindow.webContents.send(
+ "update-filename",
+ path.basename(filePath),
+ stats.birthtime, // Use birthtime (creation time) for Created timestamp
+ filePath,
+ false,
+ lastSavedTimestamp
+ );
+
+ // Send the data to the renderer
+ mainWindow.webContents.send("load-initial-data", { filePath, data });
+ }
+ }
+});
+
+ipcMain.handle("load-recent-file", async (event, filePath) => {
+ try {
+ // Trigger final auto-save if we have a proper file
+ await finalAutoSave();
+
+ // Check for unsaved changes first
+ if (await checkUnsavedChanges()) {
+ if (fs.existsSync(filePath)) {
+ const content = fs.readFileSync(filePath, "utf8");
+ const data = JSON.parse(content);
+
+ // Load the file in the current window
+ const stats = getFileStats(filePath);
+ lastSavedTimestamp = stats.mtime; // Use file's modification time as last saved
+ setCurrentFile(filePath, false);
+ mainWindow.webContents.send(
+ "update-filename",
+ path.basename(filePath),
+ stats.birthtime, // Use birthtime (creation time) for Created timestamp
+ filePath,
+ false,
+ lastSavedTimestamp
+ );
+
+ // Return the data directly to the renderer
+ return data;
+ } else {
+ console.log("File not found, removing from recent files:", filePath);
+ // Remove from recent files if it doesn't exist
+ recentFiles = recentFiles.filter(path => path !== filePath);
+ saveRecentFiles();
+ updateMenu();
+ throw new Error("File not found");
+ }
+ }
+ } catch (error) {
+ console.error("Error in load-recent-file:", error);
+ throw error;
+ }
+});
+
+// Handle renderer ready to load temp file
+ipcMain.handle("renderer-ready", async event => {
+ if (fs.existsSync(tempFilePath)) {
+ try {
+ const content = fs.readFileSync(tempFilePath, "utf8");
+ const data = JSON.parse(content);
+
+ if (hasContent(data)) {
+ setCurrentFile(tempFilePath, true);
+ lastSavedTimestamp = new Date(); // Set current time as last saved for temp file
+ mainWindow.webContents.send(
+ "update-filename",
+ "Unsaved",
+ new Date(),
+ tempFilePath,
+ true,
+ lastSavedTimestamp
+ );
+
+ // Show warning about restored temp file
+ dialog.showMessageBox(mainWindow, {
+ type: "info",
+ title: "Restored Unsaved Work",
+ message:
+ "Your previous unsaved work has been restored from a temporary file.",
+ detail:
+ "This file will be automatically saved as you work. Use 'Save As...' to save it with a proper name.",
+ });
+
+ return data;
+ }
+ } catch (error) {
+ console.error("Error loading temp file:", error);
+ }
+ }
+
+ // Set up for fresh start
+ setCurrentFile(tempFilePath, true);
+ lastSavedTimestamp = null; // No last saved time for fresh start
+ mainWindow.webContents.send(
+ "update-filename",
+ "Unsaved",
+ null,
+ tempFilePath,
+ true,
+ lastSavedTimestamp
+ );
+ return null;
+});
+
+ipcMain.handle("load-settings", async event => {
+ const miniLoomSettingsFilePath = path.join(
+ app.getPath("appData"),
+ "miniloom",
+ "settings.json"
+ );
+
+ let settings;
+ if (fs.existsSync(miniLoomSettingsFilePath)) {
+ settings = fs.readFileSync(miniLoomSettingsFilePath, "utf8");
+ const parsedSettings = JSON.parse(settings);
+ return parsedSettings;
+ } else {
+ // Return empty settings object if file doesn't exist
+ return {
+ services: {},
+ samplers: {},
+ "api-keys": {},
+ };
+ }
+});
+
+// Auto-save handler - saves to current file or temp file
+ipcMain.handle("auto-save", (event, data) => {
+ const userFileData = {
+ loomTree: data.loomTree,
+ focus: data.focus,
+ };
+
+ // Save to current file if it's a proper file, otherwise save to temp
+ const savePath =
+ currentFilePath && currentFilePath !== tempFilePath
+ ? currentFilePath
+ : tempFilePath;
+ fs.writeFileSync(savePath, JSON.stringify(userFileData));
+ lastSavedTimestamp = new Date();
+
+ // Update current file path if saving to temp
+ if (savePath === tempFilePath) {
+ currentFilePath = tempFilePath;
+ }
+
+ // Update the filename display with the new last saved timestamp
+ if (mainWindow && currentFilePath) {
+ const isTemp = currentFilePath === tempFilePath;
+ const filename = isTemp ? "Unsaved" : path.basename(currentFilePath);
+ const creationTime = isTemp
+ ? new Date()
+ : getFileStats(currentFilePath).birthtime; // Use birthtime (creation time) not mtime (modification time)
+
+ mainWindow.webContents.send(
+ "update-filename",
+ filename,
+ creationTime,
+ currentFilePath,
+ isTemp,
+ lastSavedTimestamp
+ );
+ }
+});
+
+ipcMain.handle("save-settings", (event, miniLoomSettings) => {
+ const appDataPath = app.getPath("appData");
+ const miniLoomSettingsDir = path.join(appDataPath, "miniloom");
+ const miniLoomSettingsFilePath = path.join(
+ miniLoomSettingsDir,
+ "settings.json"
+ );
+ if (!fs.existsSync(miniLoomSettingsDir)) {
+ fs.mkdirSync(miniLoomSettingsDir);
+ }
+ fs.writeFileSync(miniLoomSettingsFilePath, JSON.stringify(miniLoomSettings));
+});
+
+app
+ .whenReady()
+ .then(() => {
+ app.setName("MiniLoom");
+ if (process.platform === "darwin") {
+ app.dock.setIcon(
+ path.join(__dirname, "..", "assets/minihf_logo_no_text.png")
+ );
+ }
+ })
+ .then(createWindow);
+
+app.on("window-all-closed", function () {
+ if (process.platform !== "darwin") app.quit();
+});
+
+app.on("activate", function () {
+ if (!mainWindow) createWindow();
+});
+
+ipcMain.on("show-context-menu", event => {
+ try {
+ const contextMenu = Menu.buildFromTemplate([
+ { label: "Cut", role: "cut" },
+ { label: "Copy", role: "copy" },
+ { label: "Paste", role: "paste" },
+ { type: "separator" },
+ { label: "Select All", role: "selectAll" },
+ ]);
+
+ contextMenu.popup(BrowserWindow.fromWebContents(event.sender));
+ } catch (error) {
+ console.error("Error showing context menu:", error);
+ }
+});
diff --git a/src/model.js b/src/model.js
new file mode 100644
index 0000000..e10a87b
--- /dev/null
+++ b/src/model.js
@@ -0,0 +1,466 @@
+const dmp = {
+ patch_make: window.electronAPI.patch_make,
+ patch_apply: window.electronAPI.patch_apply,
+};
+
+/**
+ * Node class - represents an immutable node in the tree
+ * Nodes are created from files, human input, or LLM generation
+ * Once created, they are immutable except for rating & status changes
+ */
+class Node {
+ constructor(id, type, parent, patch, summary) {
+ this.id = id;
+ this.type = type; // "root", "user", "gen", "rewrite"
+ this.parent = parent;
+ this.patch = patch;
+ this.summary = summary;
+
+ this.timestamp = Date.now();
+ this.rating = null; // true = thumbs up, false = thumbs down, null = no rating
+ this.read = false;
+ this.children = [];
+ this.model = null; // Model used for generation (e.g., "gpt-4", "llama-2-70b")
+ this.finishReason = null; // Finish reason from LLM API (e.g., "stop", "length", "content_filter")
+
+ // Status tracking
+ this.generationPending = false; // Track if generation is in progress
+ this.error = null; // Track any errors
+
+ // derived values - do not serialize
+ this.cachedRenderText = null;
+ this.depth = 0; // Depth in the tree (0 = root, 1 = first level, etc.)
+ this.netCharsAdded = 0;
+ this.netWordsAdded = 0;
+ this.characterCount = 0;
+ this.wordCount = 0;
+ this.recentlyAdded = true;
+ this.treeStats = new TreeStats();
+ }
+}
+
+class TreeStats {
+ constructor() {
+ this.maxChildDepth = 0;
+ this.totalChildNodes = 0;
+ this.lastChildUpdate = 0;
+ this.maxWordCountOfChildren = 0;
+ this.maxCharCountOfChildren = 0;
+ this.unreadChildNodes = 0;
+ this.ratedUpNodes = 0;
+ this.ratedDownNodes = 0;
+ this.recentNodes = 0; // Nodes created in the last X minutes
+ }
+}
+
+class LoomTree {
+ constructor() {
+ this.root = new Node("1", "root", null, "", "Root Node");
+ this.nodeStore = { 1: this.root };
+ }
+
+ createNode(type, parent, text, summary) {
+ const parentRenderedText = this.applyPatches(parent);
+ const patch = dmp.patch_make(parentRenderedText, text);
+ const newNodeId = String(Object.keys(this.nodeStore).length + 1);
+ const newNode = new Node(newNodeId, type, parent.id, patch, summary);
+
+ if (newNode.type === "user") {
+ newNode.read = true;
+ }
+
+ parent.children.push(newNodeId);
+ this.nodeStore[newNodeId] = newNode;
+
+ this.updateNodeStats(newNode);
+ // New nodes are always recent, so add 1 to recent count
+ this.updateParentStatsIncremental(
+ newNode,
+ 1,
+ newNode.read ? 0 : 1,
+ 0,
+ 0,
+ 1
+ );
+
+ return newNode;
+ }
+
+ updateNode(node, text, summary) {
+ if (node.type === "gen" || node.children.length > 0) {
+ return; // Can't update generated nodes or nodes with children
+ }
+
+ const parent = this.nodeStore[node.parent];
+ const parentRenderedText = this.applyPatches(parent);
+ node.patch = dmp.patch_make(parentRenderedText, text);
+
+ // Store the old recent status before updating timestamp
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
+ const wasRecent = node.timestamp > fiveMinutesAgo;
+
+ node.timestamp = Date.now();
+ node.summary = summary;
+
+ this.updateNodeStats(node, fiveMinutesAgo);
+
+ // Determine if recent status changed and adjust count incrementally
+ const isNowRecent = node.timestamp > fiveMinutesAgo;
+ const recentChange = (isNowRecent ? 1 : 0) - (wasRecent ? 1 : 0);
+
+ this.updateParentStatsIncremental(node, 0, 0, 0, 0, recentChange);
+ }
+
+ updateNodeStats(node, recentTimeThreshold) {
+ const renderedText = this.applyPatches(node);
+ node.cachedRenderText = renderedText;
+
+ const textStats = window.utils.calculateTextStats(renderedText);
+ node.characterCount = textStats.charCount;
+ node.wordCount = textStats.wordCount;
+ node.recentlyAdded = node.timestamp > recentTimeThreshold;
+
+ if (node.parent) {
+ node.depth = this.nodeStore[node.parent].depth + 1;
+ const parent = this.nodeStore[node.parent];
+ node.netCharsAdded = textStats.charCount - parent.characterCount;
+ node.netWordsAdded = textStats.wordCount - parent.wordCount;
+ } else {
+ node.depth = 0;
+ node.netCharsAdded = textStats.charCount;
+ node.netWordsAdded = textStats.wordCount;
+ }
+ }
+
+ /**
+ * Render the full text of a node by applying all patches from root
+ */
+ applyPatches(node) {
+ if (!node || node === this.root) {
+ return "";
+ }
+
+ const patches = [];
+ patches.push(node.patch);
+
+ let currentNode = node;
+ while (currentNode.parent !== null) {
+ currentNode = this.nodeStore[currentNode.parent];
+ if (!currentNode) {
+ console.warn("Parent node not found in nodeStore:", node.parent);
+ break;
+ }
+ patches.push(currentNode.patch);
+ }
+
+ patches.reverse();
+ let outText = "";
+ for (let patch of patches) {
+ if (patch === "") continue;
+ const [newText, results] = dmp.patch_apply(patch, outText);
+ outText = newText;
+ }
+ node.cachedRenderText = outText;
+ return outText;
+ }
+
+ /**
+ * Alias for applyPatches - renders the full text of a node
+ */
+ renderNode(node) {
+ return this.applyPatches(node);
+ }
+
+ updateParentStatsIncremental(
+ node,
+ numNodesAdded = 0,
+ numUnreadChanged = 0,
+ numRatedUpChanged = 0,
+ numRatedDownChanged = 0,
+ numRecentChanged = 0
+ ) {
+ // when a node is added or edited, recalculate up the tree without needing to redo the full traversal
+ if (node.parent) {
+ let parentStats = this.nodeStore[node.parent].treeStats;
+ parentStats.totalChildNodes += numNodesAdded;
+
+ // For leaf nodes, use the node's own timestamp; for non-leaf nodes, use their tree's lastChildUpdate
+ const nodeLastUpdate =
+ node.children.length === 0
+ ? node.timestamp
+ : node.treeStats.lastChildUpdate;
+
+ // Update lastChildUpdate to reflect the most recent change
+ parentStats.lastChildUpdate = Math.max(
+ parentStats.lastChildUpdate || 0,
+ nodeLastUpdate
+ );
+
+ // For leaf nodes, use the node's own stats; for non-leaf nodes, use their tree stats
+ const nodeMaxWords =
+ node.children.length === 0
+ ? node.wordCount
+ : node.treeStats.maxWordCountOfChildren;
+ const nodeMaxChars =
+ node.children.length === 0
+ ? node.characterCount
+ : node.treeStats.maxCharCountOfChildren;
+ const nodeMaxDepth =
+ node.children.length === 0 ? 0 : node.treeStats.maxChildDepth;
+
+ parentStats.maxWordCountOfChildren = Math.max(
+ parentStats.maxWordCountOfChildren,
+ nodeMaxWords
+ );
+ parentStats.maxCharCountOfChildren = Math.max(
+ parentStats.maxCharCountOfChildren,
+ nodeMaxChars
+ );
+
+ // Update parent's max depth if this node's depth exceeds it
+ let newDepth = nodeMaxDepth + 1;
+ parentStats.maxChildDepth = Math.max(parentStats.maxChildDepth, newDepth);
+
+ parentStats.unreadChildNodes += numUnreadChanged;
+ parentStats.ratedUpNodes += numRatedUpChanged;
+ parentStats.ratedDownNodes += numRatedDownChanged;
+ parentStats.recentNodes += numRecentChanged;
+
+ this.updateParentStatsIncremental(
+ this.nodeStore[node.parent],
+ numNodesAdded,
+ numUnreadChanged,
+ numRatedUpChanged,
+ numRatedDownChanged,
+ numRecentChanged
+ );
+ }
+ }
+
+ updateNodeRating(nodeId, rating) {
+ const node = this.nodeStore[nodeId];
+ if (node) {
+ const previousRating = node.rating;
+ node.rating = rating;
+
+ if (previousRating != node.rating) {
+ this.updateParentStatsIncremental(
+ node,
+ 0,
+ 0,
+ node.rating == true ? 1 : previousRating == true ? -1 : 0,
+ node.rating == false ? 1 : previousRating == false ? -1 : 0,
+ 0
+ );
+ }
+ }
+ }
+
+ setNodeGenerationPending(nodeId, pending) {
+ const node = this.nodeStore[nodeId];
+ if (node) {
+ node.generationPending = pending;
+ // Update tree view if available
+ if (window.treeNav) {
+ window.treeNav.updateNodeStatus(nodeId);
+ }
+ }
+ }
+
+ setNodeError(nodeId, error) {
+ const node = this.nodeStore[nodeId];
+ if (node) {
+ node.error = error;
+ // Update tree view if available
+ if (window.treeNav) {
+ window.treeNav.updateNodeStatus(nodeId);
+ }
+ }
+ }
+
+ clearNodeError(nodeId) {
+ const node = this.nodeStore[nodeId];
+ if (node) {
+ node.error = null;
+ // Update tree view if available
+ if (window.treeNav) {
+ window.treeNav.updateNodeStatus(nodeId);
+ }
+ }
+ }
+
+ markNodeAsRead(nodeId) {
+ const node = this.nodeStore[nodeId];
+ if (node) {
+ // Calculate the change: if node was unread, subtract 1; otherwise no change
+ const wasUnread = !node.read;
+ const unreadChange = wasUnread ? -1 : 0;
+
+ node.read = true;
+
+ // Update this node's unread count (only affects the node itself, not its subtree)
+ if (wasUnread) {
+ // The node itself is no longer unread, but its subtree unread count is unchanged
+ // So the total change is -1
+ node.treeStats.unreadChildNodes = Math.max(
+ 0,
+ node.treeStats.unreadChildNodes - 1
+ );
+ }
+
+ // Propagate the change upward
+ this.updateParentStatsIncremental(node, 0, unreadChange, 0, 0, 0);
+
+ // Update tree view if available
+ if (window.treeNav) {
+ window.treeNav.updateTreeView();
+ }
+ }
+ }
+
+ serialize() {
+ const nodeStore = {};
+
+ // Serialize each node in the nodeStore, excluding temporary fields
+ Object.keys(this.nodeStore).forEach(nodeId => {
+ const node = this.nodeStore[nodeId];
+ nodeStore[nodeId] = {
+ id: node.id,
+ timestamp: node.timestamp,
+ type: node.type,
+ patch: node.patch,
+ summary: node.summary,
+ rating: node.rating,
+ read: node.read,
+ parent: node.parent,
+ children: node.children,
+ model: node.model,
+ error: node.error,
+ finishReason: node.finishReason,
+ };
+ });
+
+ return { nodeStore };
+ }
+
+ loadFromData(loomTreeData) {
+ this.nodeStore = {};
+
+ Object.keys(loomTreeData.nodeStore).forEach(nodeId => {
+ const nodeData = loomTreeData.nodeStore[nodeId];
+ const node = new Node(
+ nodeData.id,
+ nodeData.type,
+ nodeData.parent,
+ nodeData.patch,
+ nodeData.summary
+ );
+
+ node.timestamp = nodeData.timestamp;
+ node.rating = nodeData.rating;
+ node.read = nodeData.read;
+ node.children = nodeData.children || [];
+ node.model = nodeData.model;
+ node.generationPending = false;
+ node.error = nodeData.error || null;
+ node.finishReason = nodeData.finishReason || null;
+
+ this.nodeStore[nodeId] = node;
+ });
+
+ this.root = this.nodeStore["1"];
+
+ if (this.root) {
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
+ this.calculateAllNodeStats(this.root, fiveMinutesAgo);
+ }
+ }
+
+ calculateAllNodeStats(node, recentTimeThreshold) {
+ this.updateNodeStats(node, recentTimeThreshold);
+ for (const childId of node.children) {
+ if (this.nodeStore[childId]) {
+ this.calculateAllNodeStats(
+ this.nodeStore[childId],
+ recentTimeThreshold
+ );
+ }
+ }
+
+ // Calculate tree stats for this node based on its children
+ let totalChildNodes = 0;
+ let maxChildDepth = 0;
+ let maxWordCountOfChildren = node.wordCount; // Start with this node's own count
+ let maxCharCountOfChildren = node.characterCount; // Start with this node's own count
+ let lastChildUpdate = node.timestamp; // Start with this node's own timestamp
+ let unreadChildNodes = 0;
+ let ratedUpNodes = 0;
+ let ratedDownNodes = 0;
+ let recentNodes = 0;
+
+ for (const childId of node.children) {
+ const child = this.nodeStore[childId];
+ if (child) {
+ // Add child's own count plus its subtree count (but not the node itself)
+ totalChildNodes += 1 + child.treeStats.totalChildNodes;
+ // Child's max depth is its own maxChildDepth + 1
+ maxChildDepth = Math.max(
+ maxChildDepth,
+ child.treeStats.maxChildDepth + 1
+ );
+ // Use child's max word/char counts (which include the child's own counts)
+ maxWordCountOfChildren = Math.max(
+ maxWordCountOfChildren,
+ child.treeStats.maxWordCountOfChildren
+ );
+ maxCharCountOfChildren = Math.max(
+ maxCharCountOfChildren,
+ child.treeStats.maxCharCountOfChildren
+ );
+ // Use the maximum timestamp from children
+ lastChildUpdate = Math.max(
+ lastChildUpdate,
+ child.treeStats.lastChildUpdate
+ );
+ // Count unread nodes in subtree
+ if (!child.read) {
+ unreadChildNodes += 1;
+ }
+ unreadChildNodes += child.treeStats.unreadChildNodes;
+
+ // Count rated nodes in subtree
+ if (child.rating === true) {
+ ratedUpNodes += 1;
+ } else if (child.rating === false) {
+ ratedDownNodes += 1;
+ }
+ ratedUpNodes += child.treeStats.ratedUpNodes;
+ ratedDownNodes += child.treeStats.ratedDownNodes;
+
+ // Count recent nodes in subtree
+ recentNodes += child.treeStats.recentNodes;
+ }
+ }
+
+ // Count this node itself if it's recent
+ if (node.recentlyAdded) {
+ recentNodes += 1;
+ }
+
+ // Update this node's tree stats
+ node.treeStats.totalChildNodes = totalChildNodes;
+ node.treeStats.maxChildDepth = maxChildDepth;
+ node.treeStats.maxWordCountOfChildren = maxWordCountOfChildren;
+ node.treeStats.maxCharCountOfChildren = maxCharCountOfChildren;
+ node.treeStats.lastChildUpdate = lastChildUpdate;
+ node.treeStats.unreadChildNodes = unreadChildNodes;
+ node.treeStats.ratedUpNodes = ratedUpNodes;
+ node.treeStats.ratedDownNodes = ratedDownNodes;
+ node.treeStats.recentNodes = recentNodes;
+ }
+}
+
+// Export classes for use in other modules
+window.Node = Node;
+window.TreeStats = TreeStats;
+window.LoomTree = LoomTree;
diff --git a/src/preload.js b/src/preload.js
new file mode 100644
index 0000000..9b70323
--- /dev/null
+++ b/src/preload.js
@@ -0,0 +1,124 @@
+const { contextBridge, ipcRenderer } = require("electron");
+
+const DiffMatchPatch = require("diff-match-patch");
+const MiniSearch = require("minisearch");
+
+// Store MiniSearch instance in preload script
+let searchIndex = null;
+
+// Safely expose only the necessary APIs
+contextBridge.exposeInMainWorld("electronAPI", {
+ // File operations (need main process)
+ saveFile: data => ipcRenderer.invoke("save-file", data),
+ loadFile: () => ipcRenderer.invoke("load-file"),
+ loadRecentFile: filePath => ipcRenderer.invoke("load-recent-file", filePath),
+ newLoom: () => ipcRenderer.invoke("new-loom"),
+ rendererReady: () => ipcRenderer.invoke("renderer-ready"),
+ autoSave: data => ipcRenderer.invoke("auto-save", data),
+
+ // Settings operations
+ loadSettings: () => ipcRenderer.invoke("load-settings"),
+ saveSettings: settings => ipcRenderer.invoke("save-settings", settings),
+ openSettings: () => ipcRenderer.invoke("open-settings"),
+ openSettingsToTab: tabName =>
+ ipcRenderer.invoke("open-settings-to-tab", tabName),
+ closeSettingsWindow: () => ipcRenderer.send("close-settings-window"),
+
+ // Read prompt files (needed for AI operations)
+ readPromptFile: filename => ipcRenderer.invoke("read-prompt-file", filename),
+
+ // Events
+ onSettingsUpdated: callback => {
+ ipcRenderer.on("settings-updated", callback);
+ return () => ipcRenderer.removeAllListeners("settings-updated");
+ },
+ onOpenToTab: callback => {
+ ipcRenderer.on("open-to-tab", callback);
+ return () => ipcRenderer.removeAllListeners("open-to-tab");
+ },
+
+ onUpdateFilename: callback => {
+ ipcRenderer.on("update-filename", callback);
+ return () => ipcRenderer.removeAllListeners("update-filename");
+ },
+
+ onInvokeAction: callback => {
+ ipcRenderer.on("invoke-action", callback);
+ return () => ipcRenderer.removeAllListeners("invoke-action");
+ },
+
+ onLoadInitialData: callback => {
+ ipcRenderer.on("load-initial-data", callback);
+ return () => ipcRenderer.removeAllListeners("load-initial-data");
+ },
+
+ onResetToNewLoom: callback => {
+ ipcRenderer.on("reset-to-new-loom", callback);
+ return () => ipcRenderer.removeAllListeners("reset-to-new-loom");
+ },
+
+ onRequestFinalSave: callback => {
+ ipcRenderer.on("request-final-save", callback);
+ return () => ipcRenderer.removeAllListeners("request-final-save");
+ },
+
+ // Context menu
+ showContextMenu: () => ipcRenderer.send("show-context-menu"),
+
+ // Utility libraries
+ DiffMatchPatch: DiffMatchPatch,
+ MiniSearch: MiniSearch,
+
+ // DiffMatchPatch methods (since class methods don't serialize through context bridge)
+ patch_make: (text1, text2) => {
+ const dmp = new DiffMatchPatch();
+ return dmp.patch_make(text1, text2);
+ },
+ patch_apply: (patch, text) => {
+ const dmp = new DiffMatchPatch();
+ return dmp.patch_apply(patch, text);
+ },
+
+ // MiniSearch methods (managed entirely in preload script)
+ createMiniSearch: options => {
+ searchIndex = new MiniSearch(options);
+ return true; // Return success indicator
+ },
+ searchIndexAdd: document => {
+ if (searchIndex) {
+ return searchIndex.add(document);
+ }
+ return false;
+ },
+ searchIndexSearch: (query, options) => {
+ if (searchIndex) {
+ return searchIndex.search(query, options);
+ }
+ return [];
+ },
+ searchIndexAutoSuggest: (query, options) => {
+ if (searchIndex) {
+ return searchIndex.autoSuggest(query, options);
+ }
+ return [];
+ },
+ searchIndexReplace: document => {
+ if (searchIndex) {
+ return searchIndex.replace(document);
+ }
+ return false;
+ },
+ searchIndexRemove: document => {
+ if (searchIndex) {
+ return searchIndex.remove(document);
+ }
+ return false;
+ },
+ searchIndexRemoveAll: () => {
+ if (searchIndex) {
+ searchIndex.removeAll();
+ return true;
+ }
+ return false;
+ },
+});
diff --git a/src/renderer.js b/src/renderer.js
new file mode 100644
index 0000000..aef6406
--- /dev/null
+++ b/src/renderer.js
@@ -0,0 +1,780 @@
+class AppState {
+ constructor() {
+ this.loomTree = new LoomTree();
+ this.focusedNode = this.loomTree.root;
+ this.samplerSettingsStore = {};
+ this.updatingNode = false; // lock to prevent generating multiple summaries at once
+ this.secondsSinceLastSave = 0;
+ }
+
+ getFocusedNode() {
+ return this.focusedNode;
+ }
+
+ getLoomTree() {
+ return this.loomTree;
+ }
+
+ getSamplerSettingsStore() {
+ return this.samplerSettingsStore;
+ }
+
+ updateSamplerSettingsStore(newSettings) {
+ this.samplerSettingsStore = newSettings;
+ window.samplerSettingsStore = this.samplerSettingsStore;
+ }
+}
+
+// Global state instance
+const appState = new AppState();
+
+// Service instances
+let llmService;
+let treeNav;
+let searchManager;
+let fileManager;
+
+const DOM = {
+ editor: document.getElementById("editor"),
+ thumbUp: document.getElementById("thumb-up"),
+ thumbDown: document.getElementById("thumb-down"),
+ nodeSummary: document.getElementById("node-summary"),
+ nodeAuthor: document.getElementById("node-author"),
+ nodeAuthorEmoji: document.getElementById("node-author-emoji"),
+ nodeDepth: document.getElementById("node-depth"),
+ nodePosition: document.getElementById("node-position"),
+ nodeCreatedTime: document.getElementById("node-created-time"),
+ nodeTimestamp: document.getElementById("node-timestamp"),
+ nodeMetadata: document.getElementById("node-metadata"),
+ finishReason: document.getElementById("finish-reason"),
+ subtreeInfo: document.getElementById("subtree-info"),
+ subtreeTotal: document.getElementById("subtree-total"),
+ errorMsgEl: document.getElementById("error-message"),
+ errorsEl: document.getElementById("errors"),
+ errorCloseButton: document.getElementById("error-close"),
+ generateButton: document.getElementById("generate-button"),
+ serviceLabel: document.querySelector('.control-group label[title="Service"]'),
+ apiKeyLabel: document.querySelector('.control-group label[title="API Key"]'),
+ samplerLabel: document.querySelector('.control-group label[title="Sampler"]'),
+ serviceSelector: document.getElementById("service-selector"),
+ samplerSelector: document.getElementById("sampler-selector"),
+ apiKeySelector: document.getElementById("api-key-selector"),
+ die: document.getElementById("die"),
+ filenameElement: document.getElementById("current-filename"),
+ loomTreeView: document.getElementById("loom-tree-view"),
+ treeTotalNodes: document.getElementById("tree-total-nodes"),
+ treeStatsSummary: document.getElementById("tree-stats-summary"),
+ treeStatsTooltip: document.getElementById("tree-stats-tooltip"),
+ editorWordCount: document.getElementById("editor-word-count"),
+ editorWordChange: document.getElementById("editor-word-change"),
+ editorCharCount: document.getElementById("editor-char-count"),
+ editorCharChange: document.getElementById("editor-char-change"),
+};
+
+/*
+ * Updates UI focus to the node corresponding to nodeId
+ */
+function updateFocus(nodeId, reason = "unknown") {
+ const node = appState.loomTree.nodeStore[nodeId];
+ if (!node) {
+ console.warn(`Node ${nodeId} not found for focus change: ${reason}`);
+ return;
+ }
+
+ // Update state
+ appState.focusedNode = node;
+ appState.loomTree.markNodeAsRead(nodeId);
+
+ updateUI();
+
+ // Auto-scroll to bottom of editor content when focusing on a new node
+ // This ensures users see the fresh new content at the bottom
+ DOM.editor.scrollTop = DOM.editor.scrollHeight;
+
+ // Auto-save when focus changes due to content creation
+ if (reason === "editor-auto-save") {
+ fileManager.autoSave();
+ }
+}
+
+function updateUI() {
+ if (!appState.focusedNode) {
+ console.warn("No focused node to render");
+ return;
+ }
+
+ DOM.editor.value = appState.focusedNode.cachedRenderText;
+
+ updateTreeStatsDisplay();
+ updateFocusedNodeStats();
+ updateThumbState();
+ updateErrorDisplay();
+
+ if (treeNav) {
+ treeNav.updateTreeView();
+ }
+}
+
+/**
+ * Tree Statistics Display
+ */
+function updateTreeStatsDisplay() {
+ const rootNode = appState.loomTree.root;
+ const lastUpdateTime =
+ rootNode.treeStats.lastChildUpdate || new Date(rootNode.timestamp);
+
+ if (DOM.treeTotalNodes) {
+ DOM.treeTotalNodes.textContent = window.utils.formatNumber(
+ rootNode.treeStats.totalChildNodes
+ );
+ }
+ if (DOM.treeStatsSummary) {
+ const tooltipText =
+ `š Total nodes: ${window.utils.formatNumber(rootNode.treeStats.totalChildNodes)}\n` +
+ `š Max depth: ${window.utils.formatNumber(rootNode.treeStats.maxChildDepth)}\n` +
+ `š Last: ${lastUpdateTime ? window.utils.formatTimestamp(lastUpdateTime) : "N/A"}\n` +
+ `š Max words: ${window.utils.formatNumber(rootNode.treeStats.maxWordCountOfChildren)}\n` +
+ `š¤ Max chars: ${window.utils.formatNumber(rootNode.treeStats.maxCharCountOfChildren || 0)}\n` +
+ `š± Unread nodes: ${window.utils.formatNumber(rootNode.treeStats.unreadChildNodes || 0)}\n` +
+ `š Rated Good: ${window.utils.formatNumber(rootNode.treeStats.ratedUpNodes || 0)}\n` +
+ `š Rated Bad: ${window.utils.formatNumber(rootNode.treeStats.ratedDownNodes || 0)}\n` +
+ `š„ Recent nodes (5min): ${window.utils.formatNumber(rootNode.treeStats.recentNodes || 0)}`;
+ DOM.treeStatsSummary.setAttribute("data-tooltip-content", tooltipText);
+ }
+
+ // Update editor footer stats
+ if (DOM.editorWordCount)
+ DOM.editorWordCount.textContent = window.utils.formatNumber(
+ appState.focusedNode.wordCount
+ );
+ if (DOM.editorWordChange) {
+ DOM.editorWordChange.textContent = `(${window.utils.formatNetChange(appState.focusedNode.netWordsAdded)})`;
+ DOM.editorWordChange.className = window.utils.getNetChangeClass(
+ appState.focusedNode.netWordsAdded
+ );
+ }
+ if (DOM.editorCharCount)
+ DOM.editorCharCount.textContent = window.utils.formatNumber(
+ appState.focusedNode.characterCount
+ );
+ if (DOM.editorCharChange) {
+ DOM.editorCharChange.textContent = `(${window.utils.formatNetChange(appState.focusedNode.netCharsAdded)})`;
+ DOM.editorCharChange.className = window.utils.getNetChangeClass(
+ appState.focusedNode.netCharsAdded
+ );
+ }
+}
+
+/**
+ * Focused Node Statistics Display
+ */
+function updateFocusedNodeStats() {
+ const focusedNode = appState.focusedNode;
+
+ DOM.nodeSummary.textContent = focusedNode.summary || "Unsaved";
+ DOM.nodeAuthor.textContent =
+ focusedNode.type === "user" ? "Human" : focusedNode.model || "Unknown";
+ DOM.nodeAuthorEmoji.textContent = focusedNode.type === "gen" ? "š¤" : "š¤";
+ DOM.nodePosition.innerHTML = `š ${focusedNode.id}:  `;
+
+ // Update timestamp
+ const formattedDate = window.utils.formatTimestamp(focusedNode.timestamp);
+ if (DOM.nodeTimestamp) {
+ DOM.nodeTimestamp.textContent = `š ${formattedDate}`;
+ }
+
+ // Update metadata (depth only)
+ if (DOM.nodeMetadata) {
+ DOM.nodeMetadata.textContent = `š ${focusedNode.depth}`;
+ }
+
+ // Display finish reason if available
+ if (DOM.finishReason) {
+ if (
+ focusedNode.finishReason &&
+ focusedNode.type === "gen" &&
+ focusedNode.finishReason !== "length"
+ ) {
+ const finishReasonText = window.utils.getFinishReasonDisplayText(
+ focusedNode.finishReason
+ );
+ DOM.finishReason.textContent = `| š ${finishReasonText}`;
+ DOM.finishReason.style.display = "inline";
+ } else {
+ DOM.finishReason.style.display = "none";
+ }
+ }
+
+ // Update subtree info
+ if (DOM.subtreeInfo && DOM.subtreeTotal) {
+ if (focusedNode.children && focusedNode.children.length > 0) {
+ DOM.subtreeTotal.textContent = focusedNode.treeStats.totalChildNodes;
+ DOM.subtreeInfo.style.display = "inline";
+
+ DOM.subtreeInfo.setAttribute(
+ "data-tooltip",
+ window.utils.generateSubtreeTooltipText(focusedNode)
+ );
+ DOM.subtreeInfo.textContent = `š ${window.utils.formatNumber(focusedNode.treeStats.totalChildNodes)} nodes`;
+ } else {
+ DOM.subtreeInfo.style.display = "none";
+ }
+ }
+}
+
+/**
+ * Error Display Management
+ */
+function updateErrorDisplay() {
+ if (!DOM.errorMsgEl || !DOM.errorsEl || !appState.focusedNode) {
+ return;
+ }
+
+ if (appState.focusedNode.error) {
+ DOM.errorMsgEl.textContent = appState.focusedNode.error;
+ DOM.errorsEl.classList.add("has-error");
+ } else {
+ DOM.errorMsgEl.textContent = "";
+ DOM.errorsEl.classList.remove("has-error");
+ }
+}
+
+function clearFocusedNodeError() {
+ if (appState.focusedNode && appState.focusedNode.error) {
+ appState.loomTree.clearNodeError(appState.focusedNode.id);
+ updateErrorDisplay();
+ }
+}
+
+/**
+ * Search Index Management
+ */
+function updateSearchIndex(node, fullText) {
+ if (searchManager) {
+ searchManager.addNodeToSearchIndex(node, fullText);
+ }
+}
+
+function updateSearchIndexForNode(node) {
+ if (searchManager) {
+ searchManager.updateNode(node, appState.loomTree.renderNode(node));
+ }
+}
+
+/**
+ * Thumb Rating Management
+ */
+function updateThumbState() {
+ if (DOM.thumbUp && DOM.thumbDown) {
+ DOM.thumbUp.classList.remove("chosen", "thumbs-up");
+ DOM.thumbDown.classList.remove("chosen", "thumbs-down");
+
+ if (appState.focusedNode.rating === true) {
+ DOM.thumbUp.classList.add("chosen", "thumbs-up");
+ } else if (appState.focusedNode.rating === false) {
+ DOM.thumbDown.classList.add("chosen", "thumbs-down");
+ }
+ }
+}
+
+function handleThumbRating(isThumbsUp) {
+ const currentRating = appState.focusedNode.rating;
+ const targetRating = isThumbsUp ? true : false;
+ const newRating = currentRating === targetRating ? null : targetRating;
+
+ appState.loomTree.updateNodeRating(appState.focusedNode.id, newRating);
+ updateThumbState();
+ if (treeNav) {
+ treeNav.updateTreeView();
+ }
+}
+
+/**
+ * Editor Event Handlers
+ */
+function setupEditorHandlers() {
+ DOM.editor.addEventListener("input", async e => {
+ const prompt = DOM.editor.value;
+
+ // Auto-save user work when writing next prompt
+ if (
+ appState.focusedNode.children.length > 0 ||
+ ["gen", "rewrite", "root"].includes(appState.focusedNode.type)
+ ) {
+ const child = appState.loomTree.createNode(
+ "user",
+ appState.focusedNode,
+ prompt,
+ "New Node"
+ );
+
+ updateSearchIndex(child, appState.loomTree.renderNode(child));
+
+ // Update tree stats display to show new recent node
+ updateTreeStatsDisplay();
+
+ updateFocus(child.id, "editor-auto-save");
+ }
+ });
+
+ DOM.editor.addEventListener("keydown", async e => {
+ const prompt = DOM.editor.value;
+ const params = llmService.prepareGenerationParams();
+
+ // Update user node content while typing
+ if (
+ appState.focusedNode.children.length === 0 &&
+ appState.focusedNode.type === "user" &&
+ !appState.updatingNode
+ ) {
+ appState.updatingNode = true;
+ appState.loomTree.updateNode(
+ appState.focusedNode,
+ prompt,
+ appState.focusedNode.summary
+ );
+
+ updateSearchIndexForNode(appState.focusedNode);
+
+ appState.updatingNode = false;
+ }
+
+ // Update character/word count on every keystroke
+ updateTreeStatsDisplay();
+
+ // Generate summary while user is writing (every 32 characters)
+ if (prompt.length % 32 === 0) {
+ if (
+ appState.focusedNode.children.length === 0 &&
+ appState.focusedNode.type === "user" &&
+ ["base"].includes(params["sampling-method"]) &&
+ !appState.updatingNode
+ ) {
+ try {
+ appState.updatingNode = true;
+ const summary = await llmService.generateSummary(prompt);
+ appState.loomTree.updateNode(appState.focusedNode, prompt, summary);
+
+ updateSearchIndexForNode(appState.focusedNode);
+
+ appState.updatingNode = false;
+ } catch (error) {
+ console.error("Summary generation error:", error);
+ appState.updatingNode = false;
+ }
+ }
+ if (treeNav) {
+ treeNav.updateTreeView();
+ }
+ }
+
+ // Generate on Ctrl/Cmd+Enter
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
+ llmService.generateNewResponses(appState.focusedNode.id);
+ }
+ });
+
+ DOM.editor.addEventListener("contextmenu", e => {
+ e.preventDefault();
+ window.electronAPI.showContextMenu();
+ });
+}
+
+/**
+ * Settings Management
+ */
+async function loadSettings() {
+ try {
+ const data = await window.electronAPI.loadSettings();
+ appState.updateSamplerSettingsStore(data || {});
+ window.samplerSettingsStore = appState.samplerSettingsStore;
+ } catch (err) {
+ console.error("Load Settings Error:", err);
+ appState.updateSamplerSettingsStore({});
+ window.samplerSettingsStore = appState.samplerSettingsStore;
+ }
+}
+
+const onSettingsUpdated = async () => {
+ try {
+ const data = await window.electronAPI.loadSettings();
+ if (data != null) {
+ appState.updateSamplerSettingsStore(data);
+ window.samplerSettingsStore = appState.samplerSettingsStore;
+ populateServiceSelector();
+ populateSamplerSelector();
+ populateApiKeySelector();
+ renderFavoritesButtons();
+ }
+ } catch (err) {
+ console.error("Load Settings Error:", err);
+ }
+};
+
+// Electron API event handlers
+window.electronAPI.onUpdateFilename(
+ (event, filename, creationTime, filePath, isTemp, lastSavedTime) => {
+ const filenameElement = document.getElementById("current-filename");
+ if (filenameElement) {
+ // Remove .json extension for display
+ const displayName = filename.replace(/\.json$/, "");
+
+ if (isTemp) {
+ // For temp files, show "Unsaved" in red with no hover info
+ filenameElement.innerHTML = `š¾ Unsaved `;
+ filenameElement.title = ""; // No hover info for temp files
+ } else {
+ // For regular files, show filename with hover info
+ filenameElement.innerHTML = `š¾ ${displayName}`;
+
+ const tooltipLines = [`File: ${filePath || "Unknown"}`];
+
+ if (creationTime) {
+ const formattedCreationTime =
+ window.utils.formatTimestamp(creationTime);
+ tooltipLines.push(`Created: ${formattedCreationTime}`);
+ }
+
+ if (lastSavedTime) {
+ const formattedLastSavedTime =
+ window.utils.formatTimestamp(lastSavedTime);
+ tooltipLines.push(`Last Saved: ${formattedLastSavedTime}`);
+ }
+
+ filenameElement.title = tooltipLines.join("\n");
+ }
+ }
+ }
+);
+
+// Tree stats recalculation timer - recalculate every minute to update recent nodes
+function treeStatsRecalcTick() {
+ // Trigger full recalculation of tree stats to update recent nodes count
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
+ appState.loomTree.calculateAllNodeStats(
+ appState.loomTree.root,
+ fiveMinutesAgo
+ );
+
+ // Update the UI to reflect the new stats
+ updateTreeStatsDisplay();
+
+ // Re-render the tree navigation to show updated badges
+ if (treeNav) {
+ treeNav.updateTreeView();
+ }
+}
+
+var treeStatsRecalcIntervalId = setInterval(treeStatsRecalcTick, 60000); // Every minute
+
+/**
+ * Helper function for summary updates
+ */
+async function updateFocusSummary() {
+ if (
+ appState.focusedNode.type === "user" &&
+ appState.focusedNode.children.length === 0 &&
+ !appState.updatingNode
+ ) {
+ const currentFocus = appState.focusedNode;
+ const newPrompt = DOM.editor.value;
+ const prompt = appState.loomTree.renderNode(currentFocus);
+
+ appState.updatingNode = true;
+ try {
+ let summary = await llmService.generateSummary(prompt);
+ if (summary.trim() === "") {
+ summary = "Summary Not Given";
+ }
+ appState.loomTree.updateNode(currentFocus, newPrompt, summary);
+
+ updateSearchIndexForNode(currentFocus);
+ } catch (error) {
+ appState.loomTree.updateNode(
+ currentFocus,
+ newPrompt,
+ "Server Response Error"
+ );
+
+ updateSearchIndexForNode(currentFocus);
+ }
+ appState.updatingNode = false;
+ }
+}
+
+async function init() {
+ try {
+ await loadSettings();
+
+ // Initialize file manager first
+ fileManager = new FileManager({
+ appState: appState,
+ updateUI: updateUI,
+ updateSearchIndex: updateSearchIndex,
+ updateSearchIndexForNode: updateSearchIndexForNode,
+ treeNav: null, // Will be set after treeNav is created
+ searchManager: null, // Will be set after searchManager is created
+ DOM: DOM,
+ });
+
+ // Initialize file manager and load initial data
+ await fileManager.init();
+
+ // Initialize services (needed for both loaded files and fresh state)
+
+ llmService = new LLMService({
+ // Settings provider - handles configuration access
+ settingsProvider: {
+ getSamplerSettings: () => ({
+ selectedServiceName: DOM.serviceSelector?.value || "",
+ selectedSamplerName: DOM.samplerSelector?.value || "",
+ selectedApiKeyName: DOM.apiKeySelector?.value || "",
+ }),
+ getSamplerSettingsStore: () => appState.getSamplerSettingsStore(),
+ },
+
+ // Data provider - handles data access without DOM coupling
+ dataProvider: {
+ getCurrentPrompt: () => DOM.editor.value,
+ getLoomTree: () => appState.getLoomTree(),
+ getCurrentFocus: () => appState.getFocusedNode(),
+ },
+
+ // Event handlers - clean callbacks named from LLM perspective
+ eventHandlers: {
+ onGenerationStarted: nodeId => {
+ // Set loading state when generation starts
+ DOM.editor.readOnly = true;
+ if (DOM.die) {
+ DOM.die.classList.add("rolling");
+ }
+
+ // Set node generation pending and clear any errors
+ const loomTree = appState.getLoomTree();
+ loomTree.setNodeGenerationPending(nodeId, true);
+ loomTree.clearNodeError(nodeId);
+ },
+
+ onGenerationFinished: nodeId => {
+ // Clear loading state when generation finishes
+ DOM.editor.readOnly = false;
+ if (DOM.die) {
+ DOM.die.classList.remove("rolling");
+ }
+
+ // Clear node generation pending state
+ const loomTree = appState.getLoomTree();
+ loomTree.setNodeGenerationPending(nodeId, false);
+ },
+
+ onGenerationFailed: (nodeId, errorMessage) => {
+ console.warn("LLM generation failed:", errorMessage);
+
+ // Set error on the node
+ const loomTree = appState.getLoomTree();
+ loomTree.setNodeError(nodeId, errorMessage);
+
+ // Trigger UI update to show the error
+ updateErrorDisplay();
+ },
+
+ onPreGeneration: async nodeId => {
+ // Auto-save and update summary before generation
+ await fileManager.autoSaveTick();
+ await updateFocusSummary();
+ },
+
+ onNodeCreated: (nodeId, nodeData) => {
+ // Update node metadata
+ if (appState.loomTree.nodeStore[nodeId]) {
+ Object.assign(
+ appState.loomTree.nodeStore[nodeId],
+ nodeData.metadata
+ );
+ }
+
+ // Update search index
+ if (searchManager) {
+ searchManager.addNodeToSearchIndex(
+ nodeData.node,
+ nodeData.fullText
+ );
+ }
+ },
+
+ onTreeViewUpdate: () => {
+ // Update tree view to show new badges
+ if (treeNav) {
+ treeNav.updateTreeView();
+ }
+ updateTreeStatsDisplay();
+ },
+
+ onFocusChanged: (nodeId, reason) => {
+ updateFocus(nodeId, reason);
+ },
+ },
+ });
+
+ // Create tree navigation service
+ treeNav = new TreeNav(
+ nodeId => {
+ updateFocus(nodeId, "tree-navigation");
+ },
+ {
+ getFocus: () => appState.getFocusedNode(),
+ getLoomTree: () => appState.getLoomTree(),
+ }
+ );
+
+ // Create search manager
+ searchManager = new SearchManager({
+ focusOnNode: nodeId => {
+ if (nodeId) {
+ updateFocus(nodeId, "search-result");
+ } else {
+ const focusedNode = appState.getFocusedNode();
+ if (focusedNode) {
+ updateFocus(focusedNode.id, "search-result");
+ }
+ }
+ },
+ loomTree: appState.getLoomTree(),
+ treeNav: treeNav,
+ });
+
+ // Update fileManager with references to other services
+ fileManager.treeNav = treeNav;
+ fileManager.searchManager = searchManager;
+
+ // Make services globally available
+ window.llmService = llmService;
+ window.treeNav = treeNav;
+ window.searchManager = searchManager;
+ window.fileManager = fileManager;
+
+ // Initialize tree view
+ treeNav.renderTree(appState.loomTree.root, DOM.loomTreeView);
+
+ // Start file manager timers and set up event handlers
+ fileManager.startAutoSaveTimer();
+ fileManager.setupEventHandlers();
+
+ // Set up event listeners
+ window.electronAPI.onSettingsUpdated(onSettingsUpdated);
+ setupEditorHandlers();
+
+ // Populate settings selectors
+ populateServiceSelector();
+ populateSamplerSelector();
+ populateApiKeySelector();
+ renderFavoritesButtons();
+
+ // Set up additional event handlers
+ if (DOM.thumbUp) {
+ DOM.thumbUp.onclick = () => handleThumbRating(true);
+ }
+
+ if (DOM.thumbDown) {
+ DOM.thumbDown.onclick = () => handleThumbRating(false);
+ }
+
+ // Settings labels
+ if (DOM.serviceLabel) {
+ DOM.serviceLabel.style.cursor = "pointer";
+ DOM.serviceLabel.onclick = () =>
+ window.electronAPI.openSettingsToTab("services");
+ }
+
+ if (DOM.apiKeyLabel) {
+ DOM.apiKeyLabel.style.cursor = "pointer";
+ DOM.apiKeyLabel.onclick = () =>
+ window.electronAPI.openSettingsToTab("api-keys");
+ }
+
+ if (DOM.samplerLabel) {
+ DOM.samplerLabel.style.cursor = "pointer";
+ DOM.samplerLabel.onclick = () =>
+ window.electronAPI.openSettingsToTab("samplers");
+ }
+
+ // Error close button handler
+ if (DOM.errorCloseButton) {
+ DOM.errorCloseButton.onclick = clearFocusedNodeError;
+ }
+
+ // Generate button handler
+ if (DOM.generateButton) {
+ DOM.generateButton.onclick = () => {
+ if (llmService && appState.focusedNode) {
+ llmService.generateNewResponses(appState.focusedNode.id);
+ }
+ };
+ }
+
+ // Tree stats tooltip handlers
+ if (DOM.treeStatsSummary && DOM.treeStatsTooltip) {
+ DOM.treeStatsSummary.onmouseenter = () => {
+ const content = DOM.treeStatsSummary.getAttribute(
+ "data-tooltip-content"
+ );
+ if (content) {
+ DOM.treeStatsTooltip.textContent = content;
+
+ // Position tooltip below the stats element
+ const rect = DOM.treeStatsSummary.getBoundingClientRect();
+ DOM.treeStatsTooltip.style.left = rect.left + rect.width / 2 + "px";
+ DOM.treeStatsTooltip.style.top = rect.bottom + 4 + "px";
+ DOM.treeStatsTooltip.style.transform = "translateX(-50%)";
+
+ DOM.treeStatsTooltip.style.display = "block";
+ }
+ };
+
+ DOM.treeStatsSummary.onmouseleave = () => {
+ DOM.treeStatsTooltip.style.display = "none";
+ };
+
+ // Allow clicking to keep tooltip open
+ DOM.treeStatsSummary.onclick = () => {
+ const content = DOM.treeStatsSummary.getAttribute(
+ "data-tooltip-content"
+ );
+ if (content) {
+ DOM.treeStatsTooltip.textContent = content;
+
+ // Position tooltip below the stats element
+ const rect = DOM.treeStatsSummary.getBoundingClientRect();
+ DOM.treeStatsTooltip.style.left = rect.left + rect.width / 2 + "px";
+ DOM.treeStatsTooltip.style.top = rect.bottom + 4 + "px";
+ DOM.treeStatsTooltip.style.transform = "translateX(-50%)";
+
+ DOM.treeStatsTooltip.style.display = "block";
+ }
+ };
+
+ // Close tooltip when clicking outside
+ document.addEventListener("click", e => {
+ if (
+ !DOM.treeStatsSummary.contains(e.target) &&
+ !DOM.treeStatsTooltip.contains(e.target)
+ ) {
+ DOM.treeStatsTooltip.style.display = "none";
+ }
+ });
+ }
+
+ // Initial render and search index setup
+ updateUI();
+
+ // Check for new user setup
+ checkForNewUser();
+ } catch (error) {
+ console.error("Initialization failed:", error);
+ }
+}
+
+// Initialize when DOM is ready
+document.addEventListener("DOMContentLoaded", () => {
+ init();
+});
diff --git a/src/search-manager.js b/src/search-manager.js
new file mode 100644
index 0000000..6d82fa8
--- /dev/null
+++ b/src/search-manager.js
@@ -0,0 +1,349 @@
+class SearchManager {
+ constructor({ focusOnNode, loomTree, treeNav } = {}) {
+ // Private state
+ this.searchResultsMode = false;
+ this.currentSearchQuery = "";
+
+ // Required dependencies
+ this.focusOnNode = focusOnNode;
+ this.loomTree = loomTree;
+ this.treeNav = treeNav;
+
+ // Initialize when DOM is ready
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", () => this.init());
+ } else {
+ this.init();
+ }
+ }
+
+ init() {
+ // DOM elements
+ this.searchInput = document.getElementById("search-input");
+ this.searchClearButton = document.getElementById("search-clear");
+ this.searchContainer = document.getElementById("search-container");
+ this.noResultsElement = document.getElementById("search-no-results");
+ this.resultsContainer = document.getElementById("search-results-container");
+
+ if (this.loomTree) {
+ this.initializeSearchInput();
+ this.initializeSearchIndex();
+ } else {
+ console.warn("loomTree isn't available for search initialization");
+ }
+ }
+
+ // Extract meaningful content from diff patches
+ extractPatchContent(patch) {
+ if (!patch || typeof patch === "string") {
+ return patch || "";
+ }
+
+ try {
+ if (Array.isArray(patch)) {
+ return patch
+ .map(p => {
+ if (p.diffs) {
+ return p.diffs
+ .filter(diff => diff[0] === 1)
+ .map(diff => diff[1])
+ .join(" ");
+ }
+ return "";
+ })
+ .join(" ");
+ }
+ return "";
+ } catch (error) {
+ console.warn("Error extracting patch content:", error);
+ return "";
+ }
+ }
+
+ addNodeToSearchIndex(node, fullText) {
+ if (!node) return;
+
+ const patchContent = this.extractPatchContent(node.patch);
+ if (!patchContent.trim() && !node.summary) return;
+
+ try {
+ window.electronAPI.searchIndexAdd({
+ id: node.id,
+ content: patchContent,
+ summary: node.summary || "",
+ type: node.type,
+ timestamp: node.timestamp,
+ fullContent: fullText,
+ });
+ } catch (error) {
+ console.warn("Error adding node to search index:", error);
+ }
+ }
+
+ initializeSearchIndex() {
+ try {
+ window.electronAPI.createMiniSearch({
+ fields: ["content", "summary"],
+ storeFields: ["content", "summary", "type", "timestamp", "fullContent"],
+ searchOptions: {
+ boost: {
+ content: 3,
+ summary: 2,
+ },
+ prefix: true,
+ fuzzy: 0.2,
+ },
+ });
+
+ // Add all existing nodes to the index
+ Object.keys(this.loomTree.nodeStore).forEach(nodeId => {
+ const node = this.loomTree.nodeStore[nodeId];
+ if (node) {
+ this.addNodeToSearchIndex(node, this.loomTree.renderNode(node));
+ }
+ });
+ } catch (error) {
+ console.error("Failed to initialize search index:", error);
+ }
+ }
+
+ performSearch(query) {
+ if (!query.trim()) {
+ return [];
+ }
+
+ try {
+ const results = window.electronAPI.searchIndexSearch(query, {
+ boost: {
+ content: 3,
+ summary: 2,
+ },
+ prefix: true,
+ fuzzy: 0.2,
+ });
+
+ return results.map(result => {
+ const node = this.loomTree.nodeStore[result.id];
+ return {
+ node: node,
+ score: result.score,
+ highlightedContent: window.utils.highlightText(
+ result.content || this.extractPatchContent(node.patch),
+ query
+ ),
+ highlightedSummary: window.utils.highlightText(
+ result.summary || "",
+ query
+ ),
+ };
+ });
+ } catch (error) {
+ console.warn("Search error:", error);
+ return [];
+ }
+ }
+
+ renderSearchResults(query) {
+ const results = this.performSearch(query);
+
+ // Hide the tree view and show search results
+ if (this.treeNav) {
+ this.treeNav.hide();
+ }
+
+ if (results.length === 0) {
+ this.noResultsElement.style.display = "block";
+ this.resultsContainer.style.display = "none";
+ return;
+ }
+
+ // Show results container and hide no results
+ this.noResultsElement.style.display = "none";
+ this.resultsContainer.style.display = "block";
+ this.resultsContainer.innerHTML = "";
+
+ results.forEach(result => {
+ const resultItem = document.createElement("div");
+ resultItem.className = "search-result-item";
+
+ const header = document.createElement("div");
+ header.className = "search-result-header";
+ header.innerHTML = `š ${result.node.id}: ${result.highlightedSummary || result.node.summary}`;
+
+ const content = document.createElement("div");
+ content.className = "search-result-content";
+ content.innerHTML = window.utils.truncateText(
+ result.highlightedContent,
+ 150
+ );
+
+ const meta = document.createElement("div");
+ meta.className = "search-result-meta";
+ const formattedDate = window.utils.formatTimestamp(result.node.timestamp);
+
+ // Get author info
+ const authorEmoji = result.node.type === "gen" ? "š¤" : "š¤";
+ const authorName =
+ result.node.type === "user" ? "Human" : result.node.model || "Unknown";
+
+ // Add rating indicators
+ let ratingHtml = "";
+ if (result.node.rating === true) {
+ ratingHtml = 'š ';
+ } else if (result.node.rating === false) {
+ ratingHtml = 'š ';
+ }
+
+ let subtreeHtml = "";
+ if (result.node.children && result.node.children.length > 0) {
+ subtreeHtml = ` | š ${result.node.treeStats.totalChildNodes}`;
+ }
+
+ // Build the metadata line with proper separators
+ let metadataLine = `š ${formattedDate} | š ${result.node.depth}`;
+ if (ratingHtml) {
+ metadataLine += ` | ${ratingHtml}`;
+ }
+ if (subtreeHtml) {
+ metadataLine += subtreeHtml;
+ }
+
+ meta.innerHTML = `
+ ${authorEmoji} ${authorName}
+ ${metadataLine}
+ Score: ${result.score.toFixed(3)}
+ `;
+
+ resultItem.appendChild(header);
+ resultItem.appendChild(content);
+ resultItem.appendChild(meta);
+
+ resultItem.onclick = () => {
+ this.focusOnNode(result.node.id);
+ };
+
+ this.resultsContainer.appendChild(resultItem);
+ });
+ }
+
+ clearSearchState() {
+ this.currentSearchQuery = "";
+ this.searchResultsMode = false;
+
+ if (this.treeNav) {
+ this.treeNav.show();
+ }
+ if (this.noResultsElement) this.noResultsElement.style.display = "none";
+ if (this.resultsContainer) this.resultsContainer.style.display = "none";
+ }
+
+ initializeSearchInput() {
+ if (!this.searchInput || !this.searchClearButton) {
+ console.warn("Search elements not found in DOM");
+ return;
+ }
+
+ // Search as you type with debouncing
+ let searchTimeout;
+ this.searchInput.addEventListener("input", e => {
+ clearTimeout(searchTimeout);
+ const query = e.target.value;
+
+ // Show/hide clear button based on input content
+ this.searchClearButton.style.display = query.trim() ? "block" : "none";
+
+ searchTimeout = setTimeout(() => {
+ this.currentSearchQuery = query;
+ this.searchResultsMode = query.trim().length > 0;
+
+ if (this.searchResultsMode) {
+ this.renderSearchResults(query);
+ } else {
+ this.clearSearchState();
+ }
+ }, 200);
+ });
+
+ // Handle clear button click
+ this.searchClearButton.addEventListener("click", () => {
+ this.searchInput.value = "";
+ this.searchClearButton.style.display = "none";
+ this.clearSearchState();
+ });
+
+ // Handle keyboard navigation
+ this.searchInput.addEventListener("keydown", e => {
+ if (e.key === "Escape") {
+ e.target.value = "";
+ this.searchClearButton.style.display = "none";
+ this.clearSearchState();
+ } else if (e.key === "Enter") {
+ // Re-run search on Enter, even if text hasn't changed
+ const query = e.target.value.trim();
+ if (query) {
+ this.currentSearchQuery = query;
+ this.searchResultsMode = true;
+ this.renderSearchResults(query);
+ }
+ }
+ });
+
+ // Clear search and restore tree view when clicking outside
+ document.addEventListener("click", e => {
+ if (this.searchContainer && !this.searchContainer.contains(e.target)) {
+ if (this.searchResultsMode) {
+ this.clearSearchState();
+ }
+ }
+ });
+ }
+
+ updateNode(node, fullText) {
+ if (!node) return;
+
+ try {
+ window.electronAPI.searchIndexReplace({
+ id: node.id,
+ content: this.extractPatchContent(node.patch),
+ summary: node.summary || "",
+ type: node.type,
+ timestamp: node.timestamp,
+ fullContent: fullText,
+ });
+ } catch (error) {
+ console.warn("Error updating node in search index:", error);
+ }
+ }
+
+ removeNode(node) {
+ try {
+ window.electronAPI.searchIndexRemove(node);
+ } catch (error) {
+ console.warn("Error removing node from search index:", error);
+ }
+ }
+
+ rebuildIndex() {
+ window.electronAPI.searchIndexRemoveAll();
+ Object.keys(this.loomTree.nodeStore).forEach(nodeId => {
+ const node = this.loomTree.nodeStore[nodeId];
+ if (node) {
+ this.addNodeToSearchIndex(node, this.loomTree.renderNode(node));
+ }
+ });
+ }
+
+ getSearchState() {
+ return {
+ isActive: this.searchResultsMode,
+ query: this.currentSearchQuery,
+ };
+ }
+
+ updateLoomTree(newLoomTree) {
+ this.loomTree = newLoomTree;
+ }
+}
+
+// Export the SearchManager class
+window.SearchManager = SearchManager;
diff --git a/src/settings-modal.css b/src/settings-modal.css
new file mode 100644
index 0000000..67b34a3
--- /dev/null
+++ b/src/settings-modal.css
@@ -0,0 +1,402 @@
+/* ==========================================================================
+ Settings Modal Styles
+ ========================================================================== */
+
+body {
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ margin: 0;
+ padding: 0;
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+ height: 100vh;
+ overflow: hidden;
+}
+
+#settings-pane {
+ width: 100%;
+ height: 100vh;
+ background-color: var(--bg-primary);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+#settings-pane form {
+ display: flex;
+ flex-direction: column;
+ width: 200px;
+}
+
+#settings-tabs {
+ display: flex;
+ background-color: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-primary);
+ padding: 0 var(--spacing-lg);
+ margin: 0;
+ height: 48px;
+ align-items: center;
+ position: relative;
+}
+
+.tab-btn {
+ padding: var(--spacing-md) var(--spacing-lg);
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-medium);
+ color: var(--text-secondary);
+ border-bottom: 3px solid transparent;
+ transition: none;
+ border-radius: 0;
+ margin-right: 0;
+ position: relative;
+}
+
+.tab-btn:hover {
+ background-color: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.tab-btn.active {
+ color: var(--text-primary);
+ border-bottom-color: var(--button-secondary);
+ background-color: var(--bg-primary);
+ font-weight: var(--font-weight-semibold);
+ box-shadow: 0 1px 0 0 var(--bg-primary);
+}
+
+/* Small table-ish layout helpers */
+.row {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ margin-bottom: var(--spacing-lg);
+}
+.row label {
+ font-weight: var(--font-weight-medium);
+ color: var(--text-primary);
+ min-width: 90px;
+ font-size: var(--font-size-base);
+}
+.divider {
+ height: 1px;
+ background-color: var(--border-primary);
+ margin: var(--spacing-xs) 0 var(--spacing-md) 0;
+}
+.btn {
+ padding: 6px 12px;
+ border: 1px solid var(--border-secondary);
+ border-radius: 4px;
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.2s;
+ font-weight: 500;
+}
+.btn:hover {
+ background-color: var(--bg-secondary);
+ border-color: var(--border-hover);
+}
+.btn.primary {
+ background-color: var(--button-secondary);
+ border-color: var(--button-secondary);
+ color: white;
+}
+.btn.primary:hover {
+ background-color: var(--button-secondary-hover);
+ border-color: var(--button-secondary-hover);
+}
+
+/* Footer styles for success/error messages - now using CSS variables from main.css */
+.muted {
+ opacity: 0.7;
+ font-size: 0.9em;
+ color: var(--text-secondary);
+}
+/* Table styles now using CSS variables from main.css */
+.mono {
+ font-family:
+ ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
+}
+
+/* Close button styles - now using CSS variables from main.css */
+
+/* Form field styles - now using CSS variables from main.css */
+
+/* Tab visibility */
+.tab-content {
+ display: none;
+ flex: 1;
+ padding: 16px 24px;
+ overflow-y: auto;
+}
+
+.visible-tab {
+ display: flex;
+ flex-direction: column;
+}
+
+.row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+.row label {
+ font-weight: 500;
+ color: var(--text-primary);
+ min-width: 90px;
+ font-size: 14px;
+}
+
+.footer {
+ height: 40px;
+ background-color: var(--bg-secondary);
+ border-top: 1px solid var(--border-primary);
+ display: flex;
+ align-items: center;
+ padding: 0 20px;
+ font-size: 13px;
+ color: var(--text-secondary);
+}
+
+.footer.success {
+ color: var(--success);
+ background-color: var(--success-bg);
+ border-top-color: var(--success-border);
+}
+
+.footer.error {
+ color: var(--error);
+ background-color: var(--error-bg);
+ border-top-color: var(--error-border);
+}
+
+/* Tab content styling */
+.tab-content .row {
+ margin-bottom: 16px;
+}
+
+.tab-content .row label {
+ font-weight: 500;
+ color: var(--text-primary);
+ min-width: 100px;
+ margin-bottom: 0;
+}
+
+.tab-content .row input {
+ padding: 12px;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ font-size: 14px;
+ transition: all 0.2s ease;
+ min-width: 350px;
+}
+
+.tab-content .row input:focus {
+ outline: none;
+ border-color: var(--engineering-orange);
+ box-shadow: 0 0 0 2px rgba(213, 4, 2, 0.2);
+}
+
+/* Help icon styling */
+.help-icon {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ background-color: var(--text-secondary);
+ color: white;
+ border-radius: 50%;
+ text-align: center;
+ line-height: 16px;
+ font-size: 11px;
+ font-weight: bold;
+ cursor: help;
+ margin-left: 6px;
+ flex-shrink: 0;
+ transition: none;
+}
+
+.help-icon:hover {
+ background-color: var(--text-primary);
+}
+
+#key-name {
+ width: 160px !important;
+ min-width: 160px !important;
+ max-width: 160px !important;
+}
+
+.tab-content .row button {
+ margin-left: 8px;
+}
+
+/* Form actions */
+.form-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 16px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border-primary);
+}
+
+.form-actions .btn {
+ min-width: 80px;
+}
+
+/* Form field improvements */
+.form-field {
+ margin-bottom: 16px;
+}
+
+.form-field label {
+ display: block;
+ margin-bottom: 6px;
+ font-weight: 500;
+ font-size: 14px;
+ color: var(--night);
+}
+
+.form-field input,
+.form-field select {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ font-size: 14px;
+ transition: all 0.2s ease;
+ box-sizing: border-box;
+}
+
+.form-field input:focus,
+.form-field select:focus {
+ outline: none;
+ border-color: var(--engineering-orange);
+ box-shadow: 0 0 0 2px rgba(213, 4, 2, 0.2);
+}
+
+.form-field input::placeholder {
+ color: var(--text-muted);
+}
+
+select {
+ padding: 6px 10px;
+ border: 1px solid var(--border-secondary);
+ border-radius: 4px;
+ background-color: var(--bg-primary);
+ font-size: 14px;
+ color: var(--text-primary);
+ cursor: pointer;
+ transition: border-color 0.2s;
+}
+
+select:focus {
+ outline: none;
+ border-color: var(--button-secondary);
+ box-shadow: 0 0 0 2px var(--shadow-focus);
+}
+
+select:hover {
+ border-color: var(--border-hover);
+}
+
+/* API Keys table styling */
+table.keys {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 8px;
+ font-family: inherit;
+ font-size: 14px;
+}
+table.keys th,
+table.keys td {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+ padding: 12px 8px;
+ text-align: left;
+ font-family: inherit;
+}
+table.keys th {
+ font-weight: 600;
+ color: var(--text-primary);
+ background-color: var(--bg-secondary);
+}
+table.keys td {
+ color: var(--text-primary);
+}
+/* Force wrapping for secret column */
+table.keys td:nth-child(2) {
+ max-width: 300px;
+ word-wrap: break-word;
+ word-break: break-all;
+ overflow-wrap: break-word;
+}
+.mono {
+ font-family: inherit;
+}
+
+/* Favorites table styling */
+#favorites-tab table.keys input,
+#favorites-tab table.keys select {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid var(--border-secondary);
+ border-radius: 4px;
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ font-size: 13px;
+ box-sizing: border-box;
+}
+
+#favorites-tab table.keys input:focus,
+#favorites-tab table.keys select:focus {
+ outline: none;
+ border-color: var(--button-secondary);
+ box-shadow: 0 0 0 2px var(--shadow-focus);
+}
+
+#favorites-tab table.keys input::placeholder {
+ color: var(--text-muted);
+}
+
+/* Close button positioning for settings modal */
+#settings-tabs .close-btn {
+ position: absolute;
+ top: 50%;
+ right: var(--spacing-md);
+ transform: translateY(-50%);
+ z-index: 10;
+}
+
+/* Welcome message styling */
+#welcome-message {
+ background: linear-gradient(
+ 135deg,
+ var(--gradient-primary) 0%,
+ var(--gradient-secondary) 100%
+ );
+ color: white;
+ padding: 20px;
+ margin: 0;
+ text-align: center;
+}
+
+#welcome-message h2 {
+ margin: 0 0 10px 0;
+ font-size: 24px;
+}
+
+#welcome-message p {
+ margin: 0 0 15px 0;
+ font-size: 16px;
+ opacity: 0.9;
+}
+
+#welcome-message p:last-child {
+ margin: 0;
+ font-size: 14px;
+ opacity: 0.8;
+}
diff --git a/src/settings-modal.html b/src/settings-modal.html
new file mode 100644
index 0000000..efaea19
--- /dev/null
+++ b/src/settings-modal.html
@@ -0,0 +1,331 @@
+
+
+
+
+ Sampler Settings
+
+
+
+
+
+
+
+
+ š Welcome to MiniLoom!
+
+
+ To get started, please create a Service and add a valid API Key from
+ that LLM provider.
+
+
+ We've created a "Default" sampler, which you can change. Samplers
+ control how many branches are generated, how long each branch is, and
+ how creative they are.
+
+
+
+
+ š Services
+ š API Keys
+ š² Samplers
+ ā¤ļø Favorites
+ Ć
+
+
+
+
+
+
+ Select Service:
+
+ -- Select a service --
+
+ Add New
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Sampler:
+
+ -- Select a sampler --
+
+ Add New
+
+
+
+
+
+
+
+
+
+
+
+
+ Key Name
+
+ ?
+ Secret
+
+ Add
+
+
+
+
+
+
+
+
+ Name
+ Secret
+ Actions
+
+
+
+
+ No API keys saved.
+
+
+
+
+
+
+
+
+ Favorites Configuration
+ Configure up to 8 favorite combinations of Service, Key, and
+ Sampler for quick access.
+
+
+
+
+
+
+ #
+ Name
+ Service
+ Key
+ Sampler
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-modal.js b/src/settings-modal.js
new file mode 100644
index 0000000..1feb106
--- /dev/null
+++ b/src/settings-modal.js
@@ -0,0 +1,904 @@
+// DOM Elements
+const servicesTab = document.getElementById("services-tab");
+const samplersTab = document.getElementById("samplers-tab");
+const apiKeysTab = document.getElementById("api-keys-tab");
+const favoritesTab = document.getElementById("favorites-tab");
+const servicesForm = document.getElementById("services-form");
+const samplersForm = document.getElementById("samplers-form");
+const serviceSelect = document.getElementById("service-select");
+const samplerSelect = document.getElementById("sampler-select");
+
+// Default configurations
+const SERVICE_DEFAULTS = {
+ base: {
+ "sampling-method": "base",
+ "service-api-url": "http://localhost:5000/",
+ "service-model-name": "togethercomputer/llama-2-70b",
+ "service-api-delay": "3000",
+ },
+ together: {
+ "sampling-method": "together",
+ "service-api-url": "https://api.together.xyz/inference",
+ "service-model-name": "togethercomputer/llama-2-70b",
+ "service-api-delay": "3000",
+ },
+ openrouter: {
+ "sampling-method": "openrouter",
+ "service-api-url": "https://openrouter.ai/api/v1/completions",
+ "service-model-name": "deepseek/deepseek-v3-base:free",
+ "service-api-delay": "3000",
+ },
+ openai: {
+ "sampling-method": "openai",
+ "service-api-url": "https://api.openai.com/v1/completions",
+ "service-model-name": "gpt-3.5-turbo-instruct",
+ "service-api-delay": "3000",
+ },
+ "openai-chat": {
+ "sampling-method": "openai-chat",
+ "service-api-url": "https://api.openai.com/v1/chat/completions",
+ "service-model-name": "gpt-5",
+ "service-api-delay": "3000",
+ },
+ anthropic: {
+ "sampling-method": "anthropic",
+ "service-api-url": "https://api.anthropic.com/v1/messages",
+ "service-model-name": "claude-3-5-sonnet-20241022",
+ "service-api-delay": "3000",
+ },
+ google: {
+ "sampling-method": "google",
+ "service-api-url":
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent",
+ "service-model-name": "gemini-2.0-flash",
+ "service-api-delay": "3000",
+ },
+};
+
+const DEFAULT_SAMPLER = {
+ "output-branches": "2",
+ "tokens-per-branch": "256",
+ temperature: "0.9",
+ "top-p": "1",
+ "top-k": "100",
+ "repetition-penalty": "1",
+};
+
+// State
+let samplerSettingsStore = {};
+let currentEditingService = null;
+let currentEditingSampler = null;
+let originalServiceData = null;
+let originalSamplerData = null;
+let activeTab = null;
+
+// Utility functions
+function flashSaved(message, type = "success") {
+ const footer = document.getElementById("footer");
+ if (!footer) return;
+
+ footer.textContent = message;
+ footer.className = `footer ${type}`;
+
+ setTimeout(() => {
+ footer.textContent = "";
+ footer.className = "footer";
+ }, 3000);
+}
+
+function getValue(id) {
+ return document.getElementById(id)?.value?.trim() || "";
+}
+
+function setValue(id, value) {
+ const element = document.getElementById(id);
+ if (element) element.value = value;
+}
+
+function setDisplay(id, show) {
+ const element = document.getElementById(id);
+ if (element) element.style.display = show ? "inline-block" : "none";
+}
+
+// Data access functions
+function getServices() {
+ if (!samplerSettingsStore.services) {
+ samplerSettingsStore.services = {};
+ }
+ return samplerSettingsStore.services;
+}
+
+function getSamplers() {
+ if (!samplerSettingsStore.samplers) {
+ samplerSettingsStore.samplers = {};
+ }
+ return samplerSettingsStore.samplers;
+}
+
+function getApiKeys() {
+ if (!samplerSettingsStore["api-keys"]) {
+ samplerSettingsStore["api-keys"] = {};
+ }
+ return samplerSettingsStore["api-keys"];
+}
+
+function getFavorites() {
+ if (!samplerSettingsStore.favorites) {
+ samplerSettingsStore.favorites = [];
+ }
+ return samplerSettingsStore.favorites;
+}
+
+// Shared form management
+function populateSelect(selectElement, items, placeholder) {
+ selectElement.innerHTML = `${placeholder} `;
+ Object.keys(items).forEach(name => {
+ const option = document.createElement("option");
+ option.value = name;
+ option.textContent = name;
+ selectElement.appendChild(option);
+ });
+}
+
+function showForm(formElement, show = true) {
+ formElement.style.display = show ? "block" : "none";
+}
+
+// Service management
+function applyServiceDefaults(serviceType) {
+ return SERVICE_DEFAULTS[serviceType] || SERVICE_DEFAULTS.base;
+}
+
+function populateServiceSelect() {
+ populateSelect(serviceSelect, getServices(), "-- Select a service --");
+}
+
+function populateServiceForm(serviceName = null, serviceData = null) {
+ if (serviceName && serviceData) {
+ // Editing existing service
+ currentEditingService = serviceName;
+ setValue("service-name", serviceName);
+ setValue("sampling-method", serviceData["sampling-method"] || "base");
+ setValue("service-api-url", serviceData["service-api-url"] || "");
+ setValue("service-model-name", serviceData["service-model-name"] || "");
+ setValue("service-api-delay", serviceData["service-api-delay"] || "");
+ setDisplay("delete-service-btn", true);
+ originalServiceData = { ...serviceData };
+ } else {
+ // Adding new service
+ currentEditingService = null;
+ setValue("service-name", "");
+ setValue("sampling-method", "base");
+ setValue("service-api-url", "");
+ setValue("service-model-name", "");
+ setValue("service-api-delay", "");
+ setDisplay("delete-service-btn", false);
+ originalServiceData = null;
+ }
+}
+
+function applyServiceDefaultsToForm(serviceType) {
+ const defaults = applyServiceDefaults(serviceType);
+ setValue("service-api-url", defaults["service-api-url"]);
+ setValue("service-model-name", defaults["service-model-name"]);
+ setValue("service-api-delay", defaults["service-api-delay"]);
+}
+
+function saveService() {
+ const name = getValue("service-name");
+ const method = getValue("sampling-method");
+ const url = getValue("service-api-url");
+ const model = getValue("service-model-name");
+ const delay = getValue("service-api-delay");
+
+ if (!utils.validateFieldStringType(name, "modelNameType")) {
+ alert(
+ "Invalid service name. Use letters, digits, '-', '_', or '.' (max 20 characters)."
+ );
+ return;
+ }
+
+ if (!utils.validateFieldStringType(url, "URLType")) {
+ alert("Invalid API URL.");
+ return;
+ }
+
+ const services = getServices();
+ if (services[name] && name !== currentEditingService) {
+ alert("A service with this name already exists.");
+ return;
+ }
+
+ const serviceData = {
+ "sampling-method": method,
+ "service-api-url": url,
+ "service-model-name": model,
+ "service-api-delay": delay,
+ };
+
+ services[name] = serviceData;
+
+ if (currentEditingService && currentEditingService !== name) {
+ delete services[currentEditingService];
+ }
+
+ persistStore();
+ populateServiceSelect();
+ serviceSelect.value = name;
+ populateServiceForm(name, serviceData);
+
+ // Refresh favorites table if it's currently visible
+ if (activeTab === "favorites") {
+ renderFavoritesTable();
+ }
+
+ flashSaved(currentEditingService ? "Service updated." : "Service created.");
+}
+
+function deleteService() {
+ if (!currentEditingService) return;
+
+ if (
+ confirm(
+ `Are you sure you want to delete the service "${currentEditingService}"?`
+ )
+ ) {
+ const services = getServices();
+ delete services[currentEditingService];
+ persistStore();
+ populateServiceSelect();
+ showServiceForm(false);
+
+ // Refresh favorites table if it's currently visible
+ if (activeTab === "favorites") {
+ renderFavoritesTable();
+ }
+
+ flashSaved("Service deleted.");
+ }
+}
+
+function cancelService() {
+ if (originalServiceData && currentEditingService) {
+ const hasChanges = checkServiceChanges();
+ if (hasChanges) {
+ populateServiceForm(currentEditingService, originalServiceData);
+ flashSaved("Changes reverted.");
+ } else {
+ showServiceForm(false);
+ }
+ } else {
+ showServiceForm(false);
+ }
+
+ currentEditingService = null;
+ originalServiceData = null;
+}
+
+function checkServiceChanges() {
+ const currentName = getValue("service-name");
+ const currentMethod = getValue("sampling-method");
+ const currentUrl = getValue("service-api-url");
+ const currentModel = getValue("service-model-name");
+ const currentDelay = getValue("service-api-delay");
+
+ return (
+ currentName !== currentEditingService ||
+ currentMethod !== originalServiceData["sampling-method"] ||
+ currentUrl !== originalServiceData["service-api-url"] ||
+ currentModel !== originalServiceData["service-model-name"] ||
+ currentDelay !== originalServiceData["service-api-delay"]
+ );
+}
+
+function showServiceForm(show = true) {
+ showForm(servicesForm, show);
+}
+
+// Sampler management
+function getDefaultSampler() {
+ return { ...DEFAULT_SAMPLER };
+}
+
+function populateSamplerSelect() {
+ populateSelect(samplerSelect, getSamplers(), "-- Select a sampler --");
+}
+
+function populateSamplerForm(samplerName = null, samplerData = null) {
+ if (samplerName && samplerData) {
+ // Editing existing sampler
+ currentEditingSampler = samplerName;
+ setValue("sampler-name", samplerName);
+ setValue("output-branches", samplerData["output-branches"] || "");
+ setValue("tokens-per-branch", samplerData["tokens-per-branch"] || "");
+ setValue("temperature", samplerData["temperature"] || "");
+ setValue("top-p", samplerData["top-p"] || "");
+ setValue("top-k", samplerData["top-k"] || "");
+ setValue("repetition-penalty", samplerData["repetition-penalty"] || "");
+ setDisplay("delete-sampler-btn", true);
+ originalSamplerData = { ...samplerData };
+ } else {
+ // Adding new sampler
+ currentEditingSampler = null;
+ const defaults = getDefaultSampler();
+ setValue("sampler-name", "");
+ setValue("output-branches", defaults["output-branches"]);
+ setValue("tokens-per-branch", defaults["tokens-per-branch"]);
+ setValue("temperature", defaults["temperature"]);
+ setValue("top-p", defaults["top-p"]);
+ setValue("top-k", defaults["top-k"]);
+ setValue("repetition-penalty", defaults["repetition-penalty"]);
+ setDisplay("delete-sampler-btn", false);
+ originalSamplerData = null;
+ }
+}
+
+function saveSampler() {
+ const name = getValue("sampler-name");
+ const branches = getValue("output-branches");
+ const tokens = getValue("tokens-per-branch");
+ const temp = getValue("temperature");
+ const topP = getValue("top-p");
+ const topK = getValue("top-k");
+ const penalty = getValue("repetition-penalty");
+
+ if (!utils.validateFieldStringType(name, "modelNameType")) {
+ alert(
+ "Invalid sampler name. Use letters, digits, '-', '_', or '.' (max 20 characters)."
+ );
+ return;
+ }
+
+ if (
+ !utils.validateFieldStringType(branches, "intType") ||
+ !utils.validateFieldStringType(tokens, "intType") ||
+ !utils.validateFieldStringType(temp, "floatType") ||
+ !utils.validateFieldStringType(topP, "floatType") ||
+ !utils.validateFieldStringType(topK, "intType") ||
+ !utils.validateFieldStringType(penalty, "floatType")
+ ) {
+ alert("Please check your input values. Some fields have invalid formats.");
+ return;
+ }
+
+ const samplers = getSamplers();
+ if (samplers[name] && name !== currentEditingSampler) {
+ alert("A sampler with this name already exists.");
+ return;
+ }
+
+ const samplerData = {
+ "output-branches": branches,
+ "tokens-per-branch": tokens,
+ temperature: temp,
+ "top-p": topP,
+ "top-k": topK,
+ "repetition-penalty": penalty,
+ };
+
+ samplers[name] = samplerData;
+
+ if (currentEditingSampler && currentEditingSampler !== name) {
+ delete samplers[currentEditingSampler];
+ }
+
+ persistStore();
+ populateSamplerSelect();
+ samplerSelect.value = name;
+ populateSamplerForm(name, samplerData);
+
+ // Refresh favorites table if it's currently visible
+ if (activeTab === "favorites") {
+ renderFavoritesTable();
+ }
+
+ flashSaved(currentEditingSampler ? "Sampler updated." : "Sampler created.");
+}
+
+function deleteSampler() {
+ if (!currentEditingSampler) return;
+
+ const samplers = getSamplers();
+ const samplerCount = Object.keys(samplers).length;
+
+ if (samplerCount <= 1) {
+ flashSaved(
+ "Cannot delete the last sampler. At least one sampler is required.",
+ "error"
+ );
+ return;
+ }
+
+ if (
+ confirm(
+ `Are you sure you want to delete the sampler "${currentEditingSampler}"?`
+ )
+ ) {
+ delete samplers[currentEditingSampler];
+ persistStore();
+ populateSamplerSelect();
+ showSamplerForm(false);
+
+ // Refresh favorites table if it's currently visible
+ if (activeTab === "favorites") {
+ renderFavoritesTable();
+ }
+
+ flashSaved("Sampler deleted.");
+ }
+}
+
+function cancelSampler() {
+ if (originalSamplerData && currentEditingSampler) {
+ const hasChanges = checkSamplerChanges();
+ if (hasChanges) {
+ populateSamplerForm(currentEditingSampler, originalSamplerData);
+ flashSaved("Changes reverted.");
+ } else {
+ showSamplerForm(false);
+ }
+ } else {
+ showSamplerForm(false);
+ }
+
+ currentEditingSampler = null;
+ originalSamplerData = null;
+}
+
+function checkSamplerChanges() {
+ const currentName = getValue("sampler-name");
+ const currentBranches = getValue("output-branches");
+ const currentTokens = getValue("tokens-per-branch");
+ const currentTemp = getValue("temperature");
+ const currentTopP = getValue("top-p");
+ const currentTopK = getValue("top-k");
+ const currentPenalty = getValue("repetition-penalty");
+
+ return (
+ currentName !== currentEditingSampler ||
+ currentBranches !== originalSamplerData["output-branches"] ||
+ currentTokens !== originalSamplerData["tokens-per-branch"] ||
+ currentTemp !== originalSamplerData["temperature"] ||
+ currentTopP !== originalSamplerData["top-p"] ||
+ currentTopK !== originalSamplerData["top-k"] ||
+ currentPenalty !== originalSamplerData["repetition-penalty"]
+ );
+}
+
+function showSamplerForm(show = true) {
+ showForm(samplersForm, show);
+}
+
+// API Keys management
+function renderApiKeysList() {
+ const tbody = document.getElementById("keys-tbody");
+ tbody.innerHTML = "";
+ const apiKeys = getApiKeys();
+ const entries = Object.entries(apiKeys);
+
+ if (entries.length === 0) {
+ const tr = document.createElement("tr");
+ const td = document.createElement("td");
+ td.colSpan = 3;
+ td.className = "muted";
+ td.textContent = "No API keys saved.";
+ tr.appendChild(td);
+ tbody.appendChild(tr);
+ return;
+ }
+
+ entries.forEach(([name, secret]) => {
+ const tr = document.createElement("tr");
+
+ const tdName = document.createElement("td");
+ tdName.textContent = name;
+
+ const tdSecret = document.createElement("td");
+ const mask = document.createElement("span");
+ mask.textContent = "ā¢".repeat(Math.min(secret?.length || 0, 12)) || "ā";
+ mask.dataset.revealed = "0";
+ tdSecret.appendChild(mask);
+
+ const tdActions = document.createElement("td");
+ const showBtn = document.createElement("button");
+ showBtn.className = "btn";
+ showBtn.textContent = "Show";
+ showBtn.addEventListener("click", () => {
+ const revealed = mask.dataset.revealed === "1";
+ mask.textContent = revealed
+ ? "ā¢".repeat(Math.min(secret?.length || 0, 12))
+ : secret || "";
+ mask.dataset.revealed = revealed ? "0" : "1";
+ showBtn.textContent = revealed ? "Show" : "Hide";
+ });
+
+ const delBtn = document.createElement("button");
+ delBtn.className = "btn";
+ delBtn.style.marginLeft = "6px";
+ delBtn.textContent = "Delete";
+ delBtn.addEventListener("click", async () => {
+ if (confirm(`Are you sure you want to delete the API key "${name}"?`)) {
+ const apiKeys = getApiKeys();
+ delete apiKeys[name];
+ await persistStore();
+ renderApiKeysList();
+
+ // Refresh favorites table if it's currently visible
+ if (activeTab === "favorites") {
+ renderFavoritesTable();
+ }
+
+ flashSaved("API key deleted.");
+ }
+ });
+
+ tdActions.appendChild(showBtn);
+ tdActions.appendChild(delBtn);
+
+ tr.appendChild(tdName);
+ tr.appendChild(tdSecret);
+ tr.appendChild(tdActions);
+ tbody.appendChild(tr);
+ });
+}
+
+// Favorites management
+function renderFavoritesTable() {
+ const tbody = document.getElementById("favorites-tbody");
+ tbody.innerHTML = "";
+
+ const favorites = getFavorites();
+ const services = getServices();
+ const apiKeys = getApiKeys();
+ const samplers = getSamplers();
+
+ // Create 8 rows (numbered 1-8)
+ for (let i = 0; i < 8; i++) {
+ const tr = document.createElement("tr");
+ const favorite = favorites[i] || {
+ name: "",
+ service: "",
+ key: "",
+ sampler: "",
+ };
+
+ // Row number
+ const tdNumber = document.createElement("td");
+ tdNumber.textContent = i + 1;
+ tdNumber.style.textAlign = "center";
+ tdNumber.style.fontWeight = "600";
+
+ // Name input
+ const tdName = document.createElement("td");
+ const nameInput = document.createElement("input");
+ nameInput.type = "text";
+ nameInput.className = "modelNameType";
+ nameInput.placeholder = "Name";
+ nameInput.maxLength = 8;
+ nameInput.value = favorite.name || "";
+ nameInput.dataset.rowIndex = i;
+ nameInput.dataset.field = "name";
+ nameInput.addEventListener("input", updateFavorite);
+ tdName.appendChild(nameInput);
+
+ // Service dropdown
+ const tdService = document.createElement("td");
+ const serviceSelect = document.createElement("select");
+ serviceSelect.dataset.rowIndex = i;
+ serviceSelect.dataset.field = "service";
+ serviceSelect.addEventListener("change", updateFavorite);
+
+ const serviceOption = document.createElement("option");
+ serviceOption.value = "";
+ serviceOption.textContent = "-- Select Service --";
+ serviceSelect.appendChild(serviceOption);
+
+ Object.keys(services).forEach(serviceName => {
+ const option = document.createElement("option");
+ option.value = serviceName;
+ option.textContent = serviceName;
+ if (serviceName === favorite.service) {
+ option.selected = true;
+ }
+ serviceSelect.appendChild(option);
+ });
+ tdService.appendChild(serviceSelect);
+
+ // Key dropdown
+ const tdKey = document.createElement("td");
+ const keySelect = document.createElement("select");
+ keySelect.dataset.rowIndex = i;
+ keySelect.dataset.field = "key";
+ keySelect.addEventListener("change", updateFavorite);
+
+ const keyOption = document.createElement("option");
+ keyOption.value = "";
+ keyOption.textContent = "None";
+ if (favorite.key === "" || favorite.key === undefined) {
+ keyOption.selected = true;
+ }
+ keySelect.appendChild(keyOption);
+
+ Object.keys(apiKeys).forEach(keyName => {
+ const option = document.createElement("option");
+ option.value = keyName;
+ option.textContent = keyName;
+ if (keyName === favorite.key) {
+ option.selected = true;
+ }
+ keySelect.appendChild(option);
+ });
+ tdKey.appendChild(keySelect);
+
+ // Sampler dropdown
+ const tdSampler = document.createElement("td");
+ const samplerSelect = document.createElement("select");
+ samplerSelect.dataset.rowIndex = i;
+ samplerSelect.dataset.field = "sampler";
+ samplerSelect.addEventListener("change", updateFavorite);
+
+ const samplerOption = document.createElement("option");
+ samplerOption.value = "";
+ samplerOption.textContent = "-- Select Sampler --";
+ samplerSelect.appendChild(samplerOption);
+
+ Object.keys(samplers).forEach(samplerName => {
+ const option = document.createElement("option");
+ option.value = samplerName;
+ option.textContent = samplerName;
+ if (samplerName === favorite.sampler) {
+ option.selected = true;
+ }
+ samplerSelect.appendChild(option);
+ });
+ tdSampler.appendChild(samplerSelect);
+
+ tr.appendChild(tdNumber);
+ tr.appendChild(tdName);
+ tr.appendChild(tdService);
+ tr.appendChild(tdKey);
+ tr.appendChild(tdSampler);
+ tbody.appendChild(tr);
+ }
+}
+
+function updateFavorite(event) {
+ const element = event.target;
+ const rowIndex = parseInt(element.dataset.rowIndex);
+ const field = element.dataset.field;
+ let value = element.value;
+
+ // Limit name field to 8 characters
+ if (field === "name" && value.length > 8) {
+ value = value.substring(0, 8);
+ element.value = value;
+ }
+
+ const favorites = getFavorites();
+
+ // Ensure the array has enough elements
+ while (favorites.length <= rowIndex) {
+ favorites.push({ name: "", service: "", key: "", sampler: "" });
+ }
+
+ favorites[rowIndex][field] = value;
+
+ // Auto-save when a field changes
+ persistStore();
+}
+
+// Tab management
+function showTab(tabElement) {
+ servicesTab.classList.remove("visible-tab");
+ samplersTab.classList.remove("visible-tab");
+ apiKeysTab.classList.remove("visible-tab");
+ favoritesTab.classList.remove("visible-tab");
+ tabElement.classList.add("visible-tab");
+}
+
+function setActiveTab(tabName) {
+ activeTab = tabName;
+
+ for (const btn of document.querySelectorAll("#settings-tabs .tab-btn")) {
+ btn.classList.remove("active");
+ }
+
+ // Add active class to the correct button
+ const activeButton = document.querySelector(
+ `#settings-tabs .tab-btn[data-tab="${activeTab}"]`
+ );
+ if (activeButton) {
+ activeButton.classList.add("active");
+ }
+
+ if (activeTab === "services") {
+ showTab(servicesTab);
+ populateServiceSelect();
+ } else if (activeTab === "samplers") {
+ showTab(samplersTab);
+ populateSamplerSelect();
+ } else if (activeTab === "favorites") {
+ showTab(favoritesTab);
+ renderFavoritesTable();
+ } else {
+ showTab(apiKeysTab);
+ renderApiKeysList();
+ }
+}
+
+// Data persistence
+async function loadSettings() {
+ try {
+ const data = await window.electronAPI.loadSettings();
+ if (data != null) {
+ samplerSettingsStore = data;
+ }
+ } catch (err) {
+ console.error("Load Settings Error:", err);
+ }
+}
+
+async function persistStore() {
+ try {
+ await window.electronAPI.saveSettings(samplerSettingsStore);
+ } catch (err) {
+ console.error("Settings save Error:", err);
+ }
+}
+
+// New user detection
+function checkForNewUserInSettings() {
+ const hasServices =
+ samplerSettingsStore?.services &&
+ Object.keys(samplerSettingsStore.services).length > 0;
+
+ if (!hasServices) {
+ const welcomeMessage = document.getElementById("welcome-message");
+ if (welcomeMessage) {
+ welcomeMessage.style.display = "block";
+ }
+
+ setTimeout(() => {
+ showServiceForm(true);
+ populateServiceForm();
+ }, 100);
+ }
+}
+
+// Event listeners
+document.addEventListener("DOMContentLoaded", function () {
+ // Service event listeners
+ document.getElementById("add-service-btn")?.addEventListener("click", () => {
+ serviceSelect.value = "";
+ showServiceForm(true);
+ populateServiceForm();
+ });
+
+ document
+ .getElementById("save-service-btn")
+ ?.addEventListener("click", saveService);
+ document
+ .getElementById("cancel-service-btn")
+ ?.addEventListener("click", cancelService);
+ document
+ .getElementById("delete-service-btn")
+ ?.addEventListener("click", deleteService);
+
+ serviceSelect?.addEventListener("change", () => {
+ const selected = serviceSelect.value;
+ if (selected) {
+ const services = getServices();
+ const serviceData = services[selected];
+ populateServiceForm(selected, serviceData);
+ showServiceForm(true);
+ } else {
+ showServiceForm(false);
+ }
+ });
+
+ document.getElementById("sampling-method")?.addEventListener("change", () => {
+ applyServiceDefaultsToForm(
+ document.getElementById("sampling-method").value
+ );
+ });
+
+ // Sampler event listeners
+ document.getElementById("add-sampler-btn")?.addEventListener("click", () => {
+ samplerSelect.value = "";
+ showSamplerForm(true);
+ populateSamplerForm();
+ });
+
+ document
+ .getElementById("save-sampler-btn")
+ ?.addEventListener("click", saveSampler);
+ document
+ .getElementById("cancel-sampler-btn")
+ ?.addEventListener("click", cancelSampler);
+ document
+ .getElementById("delete-sampler-btn")
+ ?.addEventListener("click", deleteSampler);
+
+ samplerSelect?.addEventListener("change", () => {
+ const selected = samplerSelect.value;
+ if (selected) {
+ const samplers = getSamplers();
+ const samplerData = samplers[selected];
+ populateSamplerForm(selected, samplerData);
+ showSamplerForm(true);
+ } else {
+ showSamplerForm(false);
+ }
+ });
+
+ // API Keys event listeners
+ document
+ .getElementById("add-key-btn")
+ ?.addEventListener("click", async () => {
+ const name = getValue("key-name");
+ const value = getValue("key-value");
+
+ if (!utils.validateFieldStringType(name, "modelNameType")) {
+ alert(
+ "Invalid key name. Use letters, digits, '-', '_', or '.' (max 20 characters)."
+ );
+ return;
+ }
+ if (!value) {
+ alert("Secret cannot be empty.");
+ return;
+ }
+
+ const apiKeys = getApiKeys();
+ if (apiKeys[name]) {
+ alert("An API key with this name already exists.");
+ return;
+ }
+
+ apiKeys[name] = value;
+ await persistStore();
+ setValue("key-name", "");
+ setValue("key-value", "");
+ renderApiKeysList();
+
+ // Refresh favorites table if it's currently visible
+ if (activeTab === "favorites") {
+ renderFavoritesTable();
+ }
+
+ flashSaved("API key added.");
+ });
+
+ // Tab switching
+ document.getElementById("settings-tabs")?.addEventListener("click", e => {
+ const btn = e.target.closest(".tab-btn");
+ if (!btn) return;
+ setActiveTab(btn.dataset.tab);
+ });
+
+ // Close button
+ document.getElementById("close-settings")?.addEventListener("click", () => {
+ window.electronAPI.closeSettingsWindow();
+ });
+
+ // Listen for open-to-tab event from main process
+ window.electronAPI.onOpenToTab((event, tabName) => {
+ if (
+ tabName &&
+ ["services", "api-keys", "samplers", "favorites"].includes(tabName)
+ ) {
+ setActiveTab(tabName);
+ }
+ });
+
+ // Initialize
+ loadSettings().then(() => {
+ // Only set default tab if no tab was already set via event
+ if (!activeTab) {
+ setActiveTab("services");
+ }
+
+ // Check if this is a new user (no services configured)
+ checkForNewUserInSettings();
+ });
+});
diff --git a/src/settings.js b/src/settings.js
new file mode 100644
index 0000000..e850ae8
--- /dev/null
+++ b/src/settings.js
@@ -0,0 +1,315 @@
+// Settings UI Management
+// Handles all DOM manipulation for settings selectors and favorites
+
+function populateServiceSelector() {
+ const serviceSelector = document.getElementById("service-selector");
+ if (!serviceSelector) {
+ console.warn("Service selector not found!");
+ return;
+ }
+
+ const currentSelection = serviceSelector.value;
+ serviceSelector.innerHTML =
+ '-- Select a service -- ';
+
+ if (appState.samplerSettingsStore && appState.samplerSettingsStore.services) {
+ const services = Object.keys(appState.samplerSettingsStore.services);
+ services.forEach(serviceName => {
+ const option = document.createElement("option");
+ option.value = serviceName;
+ option.textContent = serviceName;
+ serviceSelector.appendChild(option);
+ });
+ }
+
+ if (
+ currentSelection &&
+ appState.samplerSettingsStore &&
+ appState.samplerSettingsStore.services &&
+ appState.samplerSettingsStore.services[currentSelection]
+ ) {
+ serviceSelector.value = currentSelection;
+ }
+}
+
+function populateSamplerSelector() {
+ const samplerSelector = document.getElementById("sampler-selector");
+ if (!samplerSelector) {
+ console.warn("Sampler selector not found!");
+ return;
+ }
+
+ const currentSelection = samplerSelector.value;
+ samplerSelector.innerHTML = "";
+
+ if (appState.samplerSettingsStore && appState.samplerSettingsStore.samplers) {
+ const samplers = Object.keys(appState.samplerSettingsStore.samplers);
+ samplers.forEach(samplerName => {
+ const option = document.createElement("option");
+ option.value = samplerName;
+ option.textContent = samplerName;
+ samplerSelector.appendChild(option);
+ });
+ }
+
+ if (
+ currentSelection &&
+ appState.samplerSettingsStore &&
+ appState.samplerSettingsStore.samplers &&
+ appState.samplerSettingsStore.samplers[currentSelection]
+ ) {
+ samplerSelector.value = currentSelection;
+ } else if (
+ appState.samplerSettingsStore &&
+ appState.samplerSettingsStore.samplers &&
+ appState.samplerSettingsStore.samplers["Default"]
+ ) {
+ samplerSelector.value = "Default";
+ }
+}
+
+function populateApiKeySelector() {
+ const apiKeySelector = document.getElementById("api-key-selector");
+ if (!apiKeySelector) {
+ console.warn("API key selector not found!");
+ return;
+ }
+
+ const currentSelection = apiKeySelector.value;
+ apiKeySelector.innerHTML = 'None ';
+
+ if (
+ appState.samplerSettingsStore &&
+ appState.samplerSettingsStore["api-keys"]
+ ) {
+ const apiKeys = Object.keys(appState.samplerSettingsStore["api-keys"]);
+ apiKeys.forEach(apiKeyName => {
+ const option = document.createElement("option");
+ option.value = apiKeyName;
+ option.textContent = apiKeyName;
+ apiKeySelector.appendChild(option);
+ });
+ }
+
+ if (
+ currentSelection &&
+ appState.samplerSettingsStore &&
+ appState.samplerSettingsStore["api-keys"] &&
+ appState.samplerSettingsStore["api-keys"][currentSelection]
+ ) {
+ apiKeySelector.value = currentSelection;
+ }
+}
+
+function renderFavoritesButtons() {
+ const favoritesContainer = document.getElementById("favorites-container");
+ if (!favoritesContainer) {
+ console.warn("Favorites container not found!");
+ return;
+ }
+
+ favoritesContainer.innerHTML = "";
+
+ const favorites = appState.samplerSettingsStore?.favorites || [];
+ const services = appState.samplerSettingsStore?.services || {};
+ const apiKeys = appState.samplerSettingsStore?.["api-keys"] || {};
+ const samplers = appState.samplerSettingsStore?.samplers || {};
+
+ // Create two rows of 4 buttons each
+ for (let row = 0; row < 2; row++) {
+ const favoritesRow = document.createElement("div");
+ favoritesRow.className = "favorites-row";
+
+ for (let col = 0; col < 4; col++) {
+ const index = row * 4 + col;
+ const favorite = favorites[index] || {
+ name: "",
+ service: "",
+ key: "",
+ sampler: "",
+ };
+
+ const favoriteBtn = document.createElement("button");
+ favoriteBtn.className = "favorite-btn";
+ favoriteBtn.textContent = favorite.name || `${index + 1}`;
+
+ // Check if this favorite has all required fields
+ const hasService = favorite.service && services[favorite.service];
+ const hasSampler = favorite.sampler && samplers[favorite.sampler];
+ const isComplete = hasService && hasSampler; // Key is optional
+
+ if (!isComplete || !favorite.name) {
+ favoriteBtn.classList.add("empty");
+ } else {
+ favoriteBtn.addEventListener("click", () => applyFavorite(index));
+ }
+
+ favoritesRow.appendChild(favoriteBtn);
+ }
+
+ favoritesContainer.appendChild(favoritesRow);
+ }
+}
+
+function applyFavorite(index) {
+ const favorites = appState.samplerSettingsStore?.favorites || [];
+ const favorite = favorites[index];
+
+ if (!favorite || !favorite.name) {
+ return;
+ }
+
+ // Update the selectors
+ const serviceSelector = document.getElementById("service-selector");
+ const apiKeySelector = document.getElementById("api-key-selector");
+ const samplerSelector = document.getElementById("sampler-selector");
+
+ if (serviceSelector && favorite.service) {
+ serviceSelector.value = favorite.service;
+ }
+ if (apiKeySelector) {
+ apiKeySelector.value = favorite.key || ""; // Handle "None" (empty string) case
+ }
+ if (samplerSelector && favorite.sampler) {
+ samplerSelector.value = favorite.sampler;
+ }
+
+ saveCurrentSettings();
+
+ if (llmService && appState.focusedNode) {
+ llmService.generateNewResponses(appState.focusedNode.id);
+ }
+}
+
+function saveCurrentSettings() {
+ const serviceSelector = document.getElementById("service-selector");
+ const apiKeySelector = document.getElementById("api-key-selector");
+ const samplerSelector = document.getElementById("sampler-selector");
+
+ if (!appState.samplerSettingsStore.lastUsed) {
+ appState.samplerSettingsStore.lastUsed = {};
+ }
+
+ if (serviceSelector && serviceSelector.value) {
+ appState.samplerSettingsStore.lastUsed.service = serviceSelector.value;
+ }
+ if (apiKeySelector) {
+ appState.samplerSettingsStore.lastUsed.apiKey = apiKeySelector.value || ""; // Save "None" as empty string
+ }
+ if (samplerSelector && samplerSelector.value) {
+ appState.samplerSettingsStore.lastUsed.sampler = samplerSelector.value;
+ }
+
+ window.electronAPI
+ .saveSettings(appState.samplerSettingsStore)
+ .catch(error => {
+ console.error("Failed to save last used settings:", error);
+ });
+}
+
+function addSettingsChangeListeners() {
+ const serviceSelector = document.getElementById("service-selector");
+ const apiKeySelector = document.getElementById("api-key-selector");
+ const samplerSelector = document.getElementById("sampler-selector");
+
+ if (serviceSelector) {
+ serviceSelector.addEventListener("change", saveCurrentSettings);
+ }
+ if (apiKeySelector) {
+ apiKeySelector.addEventListener("change", saveCurrentSettings);
+ }
+ if (samplerSelector) {
+ samplerSelector.addEventListener("change", saveCurrentSettings);
+ }
+}
+
+function restoreLastUsedSettings() {
+ if (!appState.samplerSettingsStore.lastUsed) {
+ return;
+ }
+
+ const lastUsed = appState.samplerSettingsStore.lastUsed;
+ if (
+ lastUsed.service &&
+ appState.samplerSettingsStore.services[lastUsed.service]
+ ) {
+ const serviceSelector = document.getElementById("service-selector");
+ if (serviceSelector) {
+ serviceSelector.value = lastUsed.service;
+ }
+ }
+
+ if (lastUsed.apiKey !== undefined) {
+ const apiKeySelector = document.getElementById("api-key-selector");
+ if (apiKeySelector) {
+ // Handle "None" (empty string) case or valid API key
+ if (
+ lastUsed.apiKey === "" ||
+ (lastUsed.apiKey &&
+ appState.samplerSettingsStore["api-keys"][lastUsed.apiKey])
+ ) {
+ apiKeySelector.value = lastUsed.apiKey;
+ }
+ }
+ }
+
+ const samplerSelector = document.getElementById("sampler-selector");
+ if (samplerSelector) {
+ if (
+ lastUsed.sampler &&
+ appState.samplerSettingsStore.samplers[lastUsed.sampler]
+ ) {
+ samplerSelector.value = lastUsed.sampler;
+ } else if (appState.samplerSettingsStore.samplers["Default"]) {
+ samplerSelector.value = "Default";
+ }
+ }
+}
+
+function checkForNewUser() {
+ const hasServices =
+ appState.samplerSettingsStore &&
+ appState.samplerSettingsStore.services &&
+ Object.keys(appState.samplerSettingsStore.services).length > 0;
+
+ if (!hasServices) {
+ // Auto-create default services if none exist
+ if (!appState.samplerSettingsStore.services) {
+ appState.samplerSettingsStore.services = {};
+ }
+
+ // Auto-create default sampler if none exist
+ if (
+ !appState.samplerSettingsStore.samplers ||
+ Object.keys(appState.samplerSettingsStore.samplers).length === 0
+ ) {
+ if (!appState.samplerSettingsStore.samplers) {
+ appState.samplerSettingsStore.samplers = {};
+ }
+
+ appState.samplerSettingsStore.samplers["Default"] = {
+ "output-branches": "2",
+ "tokens-per-branch": "256",
+ temperature: "0.9",
+ "top-p": "1",
+ "top-k": "100",
+ "repetition-penalty": "1",
+ };
+ }
+
+ try {
+ window.electronAPI.saveSettings(appState.samplerSettingsStore);
+ populateServiceSelector();
+ populateSamplerSelector();
+ } catch (error) {
+ console.error("Failed to save default settings:", error);
+ }
+
+ // Auto-open settings for new user
+ setTimeout(() => {
+ window.electronAPI.openSettings();
+ }, 500);
+ } else {
+ restoreLastUsedSettings();
+ }
+}
diff --git a/src/tree-nav.js b/src/tree-nav.js
new file mode 100644
index 0000000..6d40c02
--- /dev/null
+++ b/src/tree-nav.js
@@ -0,0 +1,462 @@
+class TreeNav {
+ constructor(onNodeClick, { getLoomTree, getFocus } = {}) {
+ this.onNodeClick = onNodeClick;
+ this.getLoomTree = getLoomTree;
+ this.getFocus = getFocus;
+
+ // Track if collapsed parents are expanded to preserve state during re-renders
+ this.collapsedParentsExpanded = false;
+ this.lastFocusedNodeId = null;
+
+ // Initialize when DOM is ready
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", () => this.init());
+ } else {
+ this.init();
+ }
+ }
+
+ init() {
+ // DOM elements
+ this.loomTreeView = document.getElementById("loom-tree-view");
+ if (!this.loomTreeView) {
+ throw new Error("loom-tree-view element not found");
+ }
+ }
+
+ renderTree(node, container) {
+ const parentIds = [];
+ const hiddenParentIds = [];
+ const loomTree = this.getLoomTree();
+
+ // Move up 2 parents to show context around the focused node
+ for (let i = 0; i < 2; i++) {
+ if (node.parent === null) {
+ break; // Stop at root node
+ }
+ parentIds.push(node.parent);
+ node = loomTree.nodeStore[node.parent];
+ }
+
+ // Find all hidden parents above the current node
+ // Walk up from the original focused node to find all parents not in parentIds
+ const originalFocus = this.getFocus();
+ let currentNode = originalFocus;
+ while (currentNode.parent !== null) {
+ const parent = loomTree.nodeStore[currentNode.parent];
+ if (parent && !parentIds.includes(parent.id)) {
+ hiddenParentIds.push(parent.id);
+ }
+ currentNode = parent;
+ }
+
+ const ul = document.createElement("ul");
+
+ // Render hidden parents if any exist
+ if (hiddenParentIds.length > 0) {
+ const hiddenParentsLi = this.createHiddenParentsLi(hiddenParentIds);
+ ul.appendChild(hiddenParentsLi);
+
+ // Add horizontal rule after hidden parents
+ const hr = document.createElement("hr");
+ hr.classList.add("hidden-parents-divider");
+ ul.appendChild(hr);
+ }
+
+ const li = this.createTreeLi(node, 1, false, parentIds);
+
+ if (node.parent) {
+ li.classList.add("hidden-parents");
+ }
+
+ ul.appendChild(li);
+ this.renderChildren(node, li, 5, parentIds);
+ container.appendChild(ul);
+ }
+
+ renderChildren(node, container, maxChildrenDepth, parentIds) {
+ if (maxChildrenDepth <= 0 || node.children.length === 0) {
+ return;
+ }
+
+ const childrenUl = document.createElement("ul");
+ const loomTree = this.getLoomTree();
+ const currentFocus = this.getFocus();
+
+ // Check if this node should be compressed (downvoted and not focused/parent)
+ const shouldCompress =
+ node.rating === false &&
+ node.id !== currentFocus.id &&
+ !parentIds.includes(node.id);
+
+ if (shouldCompress) {
+ // Create compressed children indicator
+ const compressedLi = this.createCompressedChildrenLi(node);
+ childrenUl.appendChild(compressedLi);
+ } else {
+ // Render children normally
+ for (let i = 0; i < node.children.length; i++) {
+ const child = loomTree.nodeStore[node.children[i]];
+ const li = this.createTreeLi(
+ child,
+ i + 1,
+ maxChildrenDepth <= 1,
+ parentIds
+ );
+ childrenUl.appendChild(li);
+ this.renderChildren(child, li, maxChildrenDepth - 1, parentIds);
+ }
+ }
+
+ container.appendChild(childrenUl);
+ }
+
+ createTreeLi(node, index, isMaxDepth, parentIds) {
+ const li = document.createElement("li");
+ const nodeSpan = this.createNodeSpan(node, parentIds, isMaxDepth);
+ const link = this.createNodeLink(node, index);
+
+ nodeSpan.appendChild(link);
+ li.appendChild(nodeSpan);
+
+ return li;
+ }
+
+ createNodeSpan(node, parentIds, isMaxDepth) {
+ const nodeSpan = document.createElement("span");
+ const currentFocus = this.getFocus();
+
+ // Set focused node ID
+ if (node.id === currentFocus.id) {
+ nodeSpan.id = "focused-node";
+ }
+
+ // Add data attribute for node identification
+ nodeSpan.setAttribute("data-node-id", node.id);
+
+ // Add base classes
+ nodeSpan.classList.add(`type-${node.type}`);
+
+ // Add conditional classes
+ if (parentIds && parentIds.includes(node.id)) {
+ nodeSpan.classList.add("parent-of-focused");
+ }
+
+ if (node.rating === true || node.rating === false) {
+ nodeSpan.classList.add(node.rating ? "upvoted" : "downvoted");
+ }
+
+ if (isMaxDepth && node.children && node.children.length > 0) {
+ nodeSpan.classList.add("hidden-children");
+ }
+
+ return nodeSpan;
+ }
+
+ createNodeLink(node, index) {
+ const link = document.createElement("a");
+
+ // Special treatment for Root Node
+ if (node.id === "1") {
+ link.textContent = "š³ Root Node";
+ link.classList.add("root-node-link");
+ link.title = "Root Node - Start Here";
+
+ link.onclick = event => {
+ event.stopPropagation();
+ this.onNodeClick(node.id);
+ };
+
+ return link;
+ }
+
+ const displayText = (node.summary || "").trim() || `Option ${index}`;
+
+ const statusSpan = this.createStatusSpan(node);
+ const ratingSpan = this.createRatingSpan(node);
+
+ // Add author span for user nodes
+ if (node.type === "user") {
+ const authorSpan = document.createElement("span");
+ authorSpan.classList.add("author-indicator");
+ authorSpan.textContent = "š¤ ";
+ link.appendChild(authorSpan);
+ }
+
+ const textSpan = document.createElement("span");
+
+ // Add š emoji to the text content if the node is Complete (finishReason === "stop")
+ if (node.finishReason === "stop") {
+ textSpan.textContent = displayText + " š";
+ textSpan.title = "Complete - this branch is ended";
+ } else {
+ textSpan.textContent = displayText;
+ }
+
+ // Make new/unread nodes normal weight and style
+ if (node.read === false) {
+ textSpan.classList.add("unread-node");
+ }
+
+ // Add elements in new order: status (now includes badge), author (if user), text, ratings
+ link.appendChild(statusSpan);
+
+ // Add author span for user nodes (already created above)
+ if (node.type === "user") {
+ // authorSpan already added above
+ }
+
+ link.appendChild(textSpan);
+
+ // Add rating last (on the right)
+ link.appendChild(ratingSpan);
+
+ // Create comprehensive hover tooltip
+ link.title = this.createNodeTooltip(node, displayText);
+
+ link.onclick = event => {
+ event.stopPropagation();
+ this.onNodeClick(node.id);
+ };
+
+ return link;
+ }
+
+ createStatusSpan(node) {
+ const statusSpan = document.createElement("span");
+ statusSpan.classList.add("node-status");
+
+ // Determine status - combine with badge functionality
+ if (node.error) {
+ statusSpan.classList.add("status-error");
+ statusSpan.textContent = "ā ļø";
+ } else if (node.generationPending) {
+ statusSpan.classList.add("status-pending");
+ statusSpan.textContent = "š²";
+ } else if (node.read === false) {
+ statusSpan.classList.add("status-unread");
+ statusSpan.textContent = "š±";
+ } else if (node.children && node.children.length > 0) {
+ // Show badge with subtree count
+ statusSpan.classList.add("status-badge");
+ const totalSubtreeNodes = node.treeStats.totalChildNodes;
+ statusSpan.textContent = ` ${window.utils.formatNumber(totalSubtreeNodes)}`;
+
+ // Add red class if there are recent nodes
+ if (node.treeStats.recentNodes > 0) {
+ statusSpan.classList.add("status-badge-unread");
+ }
+ } else {
+ statusSpan.classList.add("status-normal");
+ statusSpan.textContent = "";
+ }
+
+ return statusSpan;
+ }
+
+ createRatingSpan(node) {
+ const ratingSpan = document.createElement("span");
+ ratingSpan.classList.add("node-rating");
+
+ if (node.rating === true) {
+ ratingSpan.classList.add("rating-star");
+ ratingSpan.textContent = "š";
+ } else if (node.rating === false) {
+ ratingSpan.classList.add("rating-no");
+ ratingSpan.textContent = "š";
+ } else {
+ ratingSpan.classList.add("rating-none");
+ ratingSpan.textContent = "";
+ }
+
+ return ratingSpan;
+ }
+
+ createNodeTooltip(node, displayText) {
+ const lines = [];
+
+ // Node summary
+ lines.push(displayText);
+
+ // Author if available
+ if (node.type === "user") {
+ lines.push("Author: User");
+ }
+
+ // Timestamp
+ if (node.timestamp) {
+ const formattedTime = window.utils.formatTimestamp(node.timestamp);
+ lines.push(`Generated: ${formattedTime}`);
+ }
+
+ // Add subtree stats using the shared utility function
+ if (node.children && node.children.length > 0) {
+ lines.push(""); // Empty line separator
+ lines.push(window.utils.generateSubtreeTooltipText(node));
+ } else {
+ // Just the basic ID info for nodes without children
+ lines.push(`ID: ${node.id || "unknown"}`);
+ }
+
+ return lines.join("\n");
+ }
+
+ createSubtreeBadge(node) {
+ const badgeSpan = document.createElement("span");
+ badgeSpan.classList.add("subtree-badge");
+
+ // Add red class if there are recent nodes
+ if (node.treeStats.recentNodes > 0) {
+ badgeSpan.classList.add("subtree-badge-unread");
+ }
+
+ // Use the pre-calculated totalChildNodes from treeStats
+ const totalSubtreeNodes = node.treeStats.totalChildNodes;
+
+ // Get the last change timestamp for the subtree
+ const lastChangeTime = node.treeStats.lastChildUpdate || node.timestamp;
+ const formattedLastChange = window.utils.formatTimestamp(lastChangeTime);
+
+ badgeSpan.textContent = ` ${window.utils.formatNumber(totalSubtreeNodes)}`;
+ badgeSpan.title = `${window.utils.formatNumber(node.children.length)} immediate children, ${window.utils.formatNumber(totalSubtreeNodes)} total nodes in subtree${node.treeStats.unreadChildNodes > 0 ? `, ${window.utils.formatNumber(node.treeStats.unreadChildNodes)} unread` : ""}${node.treeStats.recentNodes > 0 ? `, ${window.utils.formatNumber(node.treeStats.recentNodes)} recent (5min)` : ""}\nš Last change: ${formattedLastChange}`;
+
+ return badgeSpan;
+ }
+
+ createCompressedChildrenLi(node) {
+ const li = document.createElement("li");
+ const span = document.createElement("span");
+
+ span.classList.add("compressed-children-indicator");
+ span.classList.add("tree-node");
+
+ const link = document.createElement("a");
+ const count = node.children.length;
+ const text = count === 1 ? "1 child node" : `${count} child nodes`;
+ link.innerHTML = `[${text}]`;
+ link.title = `Click to expand and show ${count} child node${count > 1 ? "s" : ""}`;
+ link.classList.add("compressed-children-link");
+
+ link.onclick = event => {
+ event.stopPropagation();
+ this.onNodeClick(node.id);
+ };
+
+ span.appendChild(link);
+ li.appendChild(span);
+
+ return li;
+ }
+
+ createHiddenParentsLi(hiddenParentIds) {
+ const li = document.createElement("li");
+
+ if (this.collapsedParentsExpanded) {
+ // Show expanded parents
+ this.createExpandedParentsContainer(hiddenParentIds, li);
+ li.classList.add("expanded");
+ } else {
+ // Show collapsed indicator
+ const span = document.createElement("span");
+ span.classList.add("hidden-parents-indicator");
+ span.classList.add("tree-node");
+
+ const link = document.createElement("a");
+ const count = hiddenParentIds.length;
+ const text =
+ count === 1 ? "1 parent node above" : `${count} parent nodes above`;
+ link.innerHTML = `ā¬ļø [${text}]`;
+ link.title = `Click to expand and show ${count} parent node${count > 1 ? "s" : ""}`;
+ link.classList.add("hidden-parents-link");
+
+ link.onclick = event => {
+ event.stopPropagation();
+ this.expandHiddenParents(hiddenParentIds, li);
+ };
+
+ span.appendChild(link);
+ li.appendChild(span);
+ }
+
+ return li;
+ }
+
+ createExpandedParentsContainer(hiddenParentIds, container) {
+ const loomTree = this.getLoomTree();
+ const expandedContainer = document.createElement("div");
+ expandedContainer.classList.add("expanded-parents-container");
+
+ // Create the parent nodes in reverse order (highest to lowest)
+ for (let i = hiddenParentIds.length - 1; i >= 0; i--) {
+ const parentId = hiddenParentIds[i];
+ const parentNode = loomTree.nodeStore[parentId];
+
+ if (parentNode) {
+ // Create a flat structure without nested ul/li to avoid tree dashes
+ const parentDiv = document.createElement("div");
+ parentDiv.classList.add("expanded-parent-node");
+
+ const nodeSpan = this.createNodeSpan(parentNode, [], false);
+ // Add parent-of-focused class to get the red styling
+ nodeSpan.classList.add("parent-of-focused");
+ const link = this.createNodeLink(parentNode, 1);
+
+ nodeSpan.appendChild(link);
+ parentDiv.appendChild(nodeSpan);
+
+ expandedContainer.appendChild(parentDiv);
+ }
+ }
+
+ container.appendChild(expandedContainer);
+ }
+
+ updateTreeView() {
+ const currentFocus = this.getFocus();
+
+ // Clear collapsed parents expansion state if we've navigated to a different node
+ if (
+ this.lastFocusedNodeId !== null &&
+ this.lastFocusedNodeId !== currentFocus.id
+ ) {
+ this.collapsedParentsExpanded = false;
+ }
+ this.lastFocusedNodeId = currentFocus.id;
+
+ this.loomTreeView.innerHTML = "";
+ this.renderTree(currentFocus, this.loomTreeView);
+ }
+
+ // Update tree view when node status changes
+ updateNodeStatus(nodeId) {
+ // Find the node element in the tree and update its status
+ const nodeElements = this.loomTreeView.querySelectorAll(
+ `[data-node-id="${nodeId}"]`
+ );
+ if (nodeElements.length > 0) {
+ // If the node is visible in the tree, update the whole tree view
+ this.updateTreeView();
+ }
+ }
+
+ expandHiddenParents(hiddenParentIds, indicatorLi) {
+ // Set the flag to preserve expansion state during re-renders
+ this.collapsedParentsExpanded = true;
+
+ // Replace the indicator with the expanded parents
+ indicatorLi.innerHTML = "";
+ this.createExpandedParentsContainer(hiddenParentIds, indicatorLi);
+ indicatorLi.classList.add("expanded");
+ }
+
+ hide() {
+ this.loomTreeView.style.display = "none";
+ }
+
+ show() {
+ this.loomTreeView.style.display = "block";
+ }
+}
+
+// Export the TreeNav class
+window.TreeNav = TreeNav;
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..081ebb5
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,282 @@
+/**
+ * Validate field values based on their expected type
+ * @param {string} fieldValue - The value to validate
+ * @param {string} fieldType - The expected type (intType, floatType, modelNameType, URLType, etc.)
+ * @returns {boolean|RegExpMatchArray|null} - Validation result
+ */
+function validateFieldStringType(fieldValue, fieldType) {
+ const fieldValueString = String(fieldValue);
+ const intPattern = /^[0-9]+$/;
+ const floatPattern = /^[0-9]+\.?[0-9]*$/;
+ const modelNamePattern = /^[a-zA-Z0-9-\._]+$/;
+
+ let result;
+ if (fieldType === "intType") {
+ result = fieldValueString.match(intPattern);
+ } else if (fieldType === "floatType") {
+ result = fieldValueString.match(floatPattern);
+ } else if (fieldType === "modelNameType") {
+ result =
+ fieldValueString.match(modelNamePattern) && fieldValueString.length <= 20;
+ } else if (fieldType === "settingsNameType") {
+ result = fieldValueString.match(modelNamePattern);
+ } else if (fieldType === "URLType") {
+ result = isValidUrl(fieldValue);
+ } else if (fieldType === "select") {
+ result = fieldValue && fieldValue.length > 0;
+ } else {
+ if (fieldType === "") {
+ console.warn(
+ "Tried to validate empty field type. Did you forget to type your form field?"
+ );
+ result = null;
+ } else {
+ console.warn("Attempted to validate unknown field type");
+ result = null;
+ }
+ }
+ return result;
+}
+
+/**
+ * Validate if a string is a valid URL
+ * @param {string} urlString - The URL string to validate
+ * @returns {boolean} - True if valid URL, false otherwise
+ */
+function isValidUrl(urlString) {
+ try {
+ new URL(urlString);
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
+
+/**
+ * Count characters in text
+ * @param {string} text - The text to count characters in
+ * @returns {number} - Number of characters
+ */
+function countCharacters(text) {
+ return text.length;
+}
+
+/**
+ * Count words in text
+ * @param {string} text - The text to count words in
+ * @returns {number} - Number of words
+ */
+function countWords(text) {
+ return text
+ .trim()
+ .split(/\s+/)
+ .filter(word => word.length > 0).length;
+}
+
+/**
+ * Simple text highlighting function for search results
+ * @param {string} text - The text to highlight
+ * @param {string} query - The search query to highlight
+ * @returns {string} - HTML with highlighted text
+ */
+function highlightText(text, query) {
+ if (!query.trim() || !text) return text;
+
+ const words = query.toLowerCase().split(/\s+/);
+ let highlighted = text;
+
+ words.forEach(word => {
+ const regex = new RegExp(`(${escapeRegex(word)})`, "gi");
+ highlighted = highlighted.replace(regex, "$1 ");
+ });
+
+ return highlighted;
+}
+
+/**
+ * Escape special regex characters in a string
+ * @param {string} string - The string to escape
+ * @returns {string} - Escaped string safe for regex
+ */
+function escapeRegex(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+/**
+ * Format a timestamp for display
+ * @param {number} timestamp - Unix timestamp
+ * @returns {string} - Formatted date string in "d MMM yy HH:mm" format
+ */
+function formatTimestamp(timestamp) {
+ return new Date(timestamp).toLocaleString("en-GB", {
+ day: "numeric",
+ month: "short",
+ year: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+/**
+ * Truncate text to a specified length with ellipsis
+ * @param {string} text - The text to truncate
+ * @param {number} maxLength - Maximum length before truncation
+ * @returns {string} - Truncated text with ellipsis if needed
+ */
+function truncateText(text, maxLength) {
+ if (!text || text.length <= maxLength) return text;
+ return text.substring(0, maxLength) + "...";
+}
+
+/**
+ * Extract the first three words from the first line of text
+ * @param {string} text - The text to extract words from
+ * @returns {string} - First three words joined by spaces
+ */
+function extractThreeWords(text) {
+ return text.trim().split("\n")[0].split(" ").slice(0, 3).join(" ");
+}
+
+/**
+ * Calculate comprehensive text statistics for a given text
+ * @param {string} text - The text to analyze
+ * @returns {Object} - Object containing wordCount, charCount, and other stats
+ */
+function calculateTextStats(text) {
+ const trimmedText = text.trim();
+ const wordCount =
+ trimmedText.length === 0 ? 0 : trimmedText.split(/\s+/).length;
+ const charCount = text.length;
+
+ return {
+ wordCount,
+ charCount,
+ isEmpty: trimmedText.length === 0,
+ lineCount: text.split("\n").length,
+ averageWordsPerLine: wordCount / Math.max(1, text.split("\n").length),
+ };
+}
+
+/**
+ * Calculate net changes between two texts
+ * @param {string} currentText - The current text
+ * @param {string} previousText - The previous text to compare against
+ * @returns {Object} - Object containing net word and character changes
+ */
+function calculateNetChanges(currentText, previousText) {
+ const currentStats = calculateTextStats(currentText);
+ const previousStats = calculateTextStats(previousText);
+
+ return {
+ netWordsChange: currentStats.wordCount - previousStats.wordCount,
+ netCharsChange: currentStats.charCount - previousStats.charCount,
+ netLinesChange: currentStats.lineCount - previousStats.lineCount,
+ };
+}
+
+/**
+ * Format number with thousands separators for display
+ * @param {number} num - The number to format
+ * @returns {string} - Formatted number with commas as thousands separators
+ */
+function formatNumber(num) {
+ return num.toLocaleString();
+}
+
+/**
+ * Format net change for display with +/- prefix and number separators
+ * @param {number} change - The net change value
+ * @returns {string} - Formatted string with +/- prefix and separators
+ */
+function formatNetChange(change) {
+ return `${change > 0 ? "+" : ""}${formatNumber(change)}`;
+}
+
+/**
+ * Get CSS class for net change styling
+ * @param {number} change - The net change value
+ * @returns {string} - CSS class name for styling
+ */
+function getNetChangeClass(change) {
+ if (change > 0) return "positive";
+ if (change < 0) return "negative";
+ return "";
+}
+
+/**
+ * Generate subtree tooltip text with comprehensive statistics
+ * @param {Object} node - The node object with treeStats
+ * @returns {string} - Formatted subtree statistics text
+ */
+function generateSubtreeTooltipText(node) {
+ if (!node.children || node.children.length === 0) {
+ return `ID: ${node.id || "unknown"}`;
+ }
+
+ const subtreeDate = formatTimestamp(
+ node.treeStats.lastChildUpdate || node.timestamp
+ );
+
+ return (
+ `š Total nodes: ${formatNumber(node.treeStats.totalChildNodes)}\n` +
+ `š¶ Direct children: ${formatNumber(node.children.length)}\n` +
+ `š Max depth: ${formatNumber(node.treeStats.maxChildDepth)}\n` +
+ `š Last: ${subtreeDate}\n` +
+ `š Max words: ${formatNumber(node.treeStats.maxWordCountOfChildren)}\n` +
+ `š¤ Max chars: ${formatNumber(node.treeStats.maxCharCountOfChildren)}\n` +
+ `š± Unread nodes: ${formatNumber(node.treeStats.unreadChildNodes || 0)}\n` +
+ `š Rated Good: ${formatNumber(node.treeStats.ratedUpNodes || 0)}\n` +
+ `š Rated Bad: ${formatNumber(node.treeStats.ratedDownNodes || 0)}\n` +
+ `š„ Recent nodes (5min): ${formatNumber(node.treeStats.recentNodes || 0)}`
+ );
+}
+
+/**
+ * Helper function to convert finish reason codes to user-friendly text
+ */
+function getFinishReasonDisplayText(finishReason) {
+ const reasonMap = {
+ stop: "Complete",
+ length: "Max Length",
+ content_filter: "Content Filtered",
+ tool_calls: "Tool Called",
+ function_call: "Function Called",
+ max_tokens: "Token Limit",
+ timeout: "Timed Out",
+ user: "User Stopped",
+ assistant: "Assistant Stopped",
+ system: "System Stopped",
+ end_turn: "Turn Ended",
+ max_content_length: "Content Limit",
+ safety: "Safety Filter",
+ recitation: "Recitation Detected",
+ network_error: "Network Error",
+ server_error: "Server Error",
+ rate_limit: "Rate Limited",
+ invalid_request: "Invalid Request",
+ unknown: "Unknown",
+ };
+
+ return reasonMap[finishReason] || finishReason || "Unknown";
+}
+
+if (typeof window !== "undefined") {
+ window.utils = {
+ validateFieldStringType,
+ isValidUrl,
+ countCharacters,
+ countWords,
+ highlightText,
+ escapeRegex,
+ formatTimestamp,
+ truncateText,
+ extractThreeWords,
+ calculateTextStats,
+ calculateNetChanges,
+ formatNumber,
+ formatNetChange,
+ getNetChangeClass,
+ generateSubtreeTooltipText,
+ getFinishReasonDisplayText,
+ };
+}