diff --git a/.gitignore b/.gitignore index 3cc71bcc6..d3c89f071 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ datasets/ node_modules/ dist/ -!installer/dist/ **/_tree-sitter/ *.log .DS_Store diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..1aab776ad --- /dev/null +++ b/.npmignore @@ -0,0 +1,48 @@ +# Source code (dist/ and plugin/ are the shipped artifacts) +src/ +scripts/ +tests/ +docs/ +datasets/ +private/ +antipattern-czar/ + +# Heavy binaries installed at runtime via smart-install.js +plugin/node_modules/ +plugin/scripts/claude-mem +plugin/bun.lock +plugin/data/ +plugin/data.backup/ + +# Development files +*.ts +!*.d.ts +tsconfig*.json +.eslintrc* +.prettierrc* +.editorconfig +jest.config* +vitest.config* + +# Git and CI +.git/ +.github/ +.gitignore +.claude/ +.cursor/ +.mcp.json +.plan/ + +# OS files +.DS_Store +*.log +*.tmp +*.temp +Thumbs.db + +# Misc +Auto Run Docs/ +~*/ +http*/ +https*/ +.idea/ diff --git a/README.md b/README.md index 834d1d2f6..e2e251cde 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,13 @@ ## Quick Start -Start a new Claude Code session in the terminal and enter the following commands: +Install with a single command: + +```bash +npx claude-mem install +``` + +Or install from the plugin marketplace inside Claude Code: ``` /plugin marketplace add thedotmack/claude-mem @@ -120,7 +126,7 @@ Start a new Claude Code session in the terminal and enter the following commands Restart Claude Code. Context from previous sessions will automatically appear in new sessions. -> **Note:** Claude-Mem is also published on npm, but `npm install -g claude-mem` installs the **SDK/library only** — it does not register the plugin hooks or set up the worker service. To use Claude-Mem as a plugin, always install via the `/plugin` commands above. +> **Note:** Claude-Mem is also published on npm, but `npm install -g claude-mem` installs the **SDK/library only** — it does not register the plugin hooks or set up the worker service. Always install via `npx claude-mem install` or the `/plugin` commands above. ### 🦞 OpenClaw Gateway diff --git a/docs/public/installation.mdx b/docs/public/installation.mdx index 9d8a8ada1..0db502806 100644 --- a/docs/public/installation.mdx +++ b/docs/public/installation.mdx @@ -7,24 +7,35 @@ description: "Install Claude-Mem plugin for persistent memory across sessions" ## Quick Start -Install Claude-Mem directly from the plugin marketplace: +### Option 1: npx (Recommended) + +Install and configure Claude-Mem with a single command: + +```bash +npx claude-mem install +``` + +The interactive installer will: +- Detect your installed IDEs (Claude Code, Cursor, Gemini CLI, Windsurf, etc.) +- Copy plugin files to the correct locations +- Register the plugin with Claude Code +- Install all dependencies (including Bun and uv) +- Auto-start the worker service + +### Option 2: Plugin Marketplace + +Install Claude-Mem directly from the plugin marketplace inside Claude Code: ```bash /plugin marketplace add thedotmack/claude-mem /plugin install claude-mem ``` -That's it! The plugin will automatically: -- Download prebuilt binaries (no compilation needed) -- Install all dependencies (including SQLite binaries) -- Configure hooks for session lifecycle management -- Auto-start the worker service on first session - -Start a new Claude Code session and you'll see context from previous sessions automatically loaded. +Both methods will automatically configure hooks and start the worker service. Start a new Claude Code session and you'll see context from previous sessions automatically loaded. > **Important:** Claude-Mem is published on npm, but running `npm install -g claude-mem` installs the > **SDK/library only**. It does **not** register plugin hooks or start the worker service. -> To use Claude-Mem as a persistent memory plugin, always install via the `/plugin` commands above. +> Always install via `npx claude-mem install` or the `/plugin` commands above. ## System Requirements diff --git a/docs/public/introduction.mdx b/docs/public/introduction.mdx index ff0d2eed0..e4605c0aa 100644 --- a/docs/public/introduction.mdx +++ b/docs/public/introduction.mdx @@ -11,7 +11,13 @@ Claude-Mem seamlessly preserves context across sessions by automatically capturi ## Quick Start -Start a new Claude Code session in the terminal and enter the following commands: +Install with a single command: + +```bash +npx claude-mem install +``` + +Or install from the plugin marketplace inside Claude Code: ```bash /plugin marketplace add thedotmack/claude-mem diff --git a/install/public/install.sh b/install/public/install.sh index 76238647f..d3be8cfa7 100644 --- a/install/public/install.sh +++ b/install/public/install.sh @@ -1,59 +1,25 @@ #!/bin/bash set -euo pipefail -# claude-mem installer bootstrap -# Usage: curl -fsSL https://install.cmem.ai | bash -# or: curl -fsSL https://install.cmem.ai | bash -s -- --provider=gemini --api-key=YOUR_KEY - -INSTALLER_URL="https://install.cmem.ai/installer.js" +# claude-mem installer redirect +# The old curl-pipe-bash installer has been replaced by npx claude-mem. +# This script now redirects users to the new install method. # Colors RED='\033[0;31m' GREEN='\033[0;32m' CYAN='\033[0;36m' +YELLOW='\033[0;33m' NC='\033[0m' # No Color -error() { echo -e "${RED}Error: $1${NC}" >&2; exit 1; } -info() { echo -e "${CYAN}$1${NC}"; } - -# Check Node.js -if ! command -v node &> /dev/null; then - error "Node.js is required but not found. Install from https://nodejs.org" -fi - -NODE_VERSION=$(node -v | sed 's/v//') -NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1) -if [ "$NODE_MAJOR" -lt 18 ]; then - error "Node.js >= 18 required. Current: v${NODE_VERSION}" -fi - -info "claude-mem installer (Node.js v${NODE_VERSION})" - -# Create temp file for installer -TMPFILE=$(mktemp "${TMPDIR:-/tmp}/claude-mem-installer.XXXXXX.mjs") - -# Cleanup on exit -cleanup() { - rm -f "$TMPFILE" -} -trap cleanup EXIT INT TERM - -# Download installer -info "Downloading installer..." -if command -v curl &> /dev/null; then - curl -fsSL "$INSTALLER_URL" -o "$TMPFILE" -elif command -v wget &> /dev/null; then - wget -q "$INSTALLER_URL" -O "$TMPFILE" -else - error "curl or wget required to download installer" -fi - -# Run installer with TTY access -# When piped (curl | bash), stdin is the script. We need to reconnect to the terminal. -if [ -t 0 ]; then - # Already have TTY (script was downloaded and run directly) - node "$TMPFILE" "$@" -else - # Piped execution -- reconnect stdin to terminal - node "$TMPFILE" "$@" = 18. Get it from ${CYAN}https://nodejs.org${NC}" +echo "" +echo -e "For more info, visit: ${CYAN}https://docs.claude-mem.ai/installation${NC}" +echo "" diff --git a/install/public/installer.js b/install/public/installer.js index 739292992..3f31d2993 100644 --- a/install/public/installer.js +++ b/install/public/installer.js @@ -1,2107 +1,17 @@ #!/usr/bin/env node -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); -// node_modules/picocolors/picocolors.js -var require_picocolors = __commonJS({ - "node_modules/picocolors/picocolors.js"(exports, module) { - var p = process || {}; - var argv = p.argv || []; - var env = p.env || {}; - var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI); - var formatter = (open, close, replace = open) => (input) => { - let string = "" + input, index = string.indexOf(close, open.length); - return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close; - }; - var replaceClose = (string, close, replace, index) => { - let result = "", cursor = 0; - do { - result += string.substring(cursor, index) + replace; - cursor = index + close.length; - index = string.indexOf(close, cursor); - } while (~index); - return result + string.substring(cursor); - }; - var createColors = (enabled = isColorSupported) => { - let f = enabled ? formatter : () => String; - return { - isColorSupported: enabled, - reset: f("\x1B[0m", "\x1B[0m"), - bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"), - dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"), - italic: f("\x1B[3m", "\x1B[23m"), - underline: f("\x1B[4m", "\x1B[24m"), - inverse: f("\x1B[7m", "\x1B[27m"), - hidden: f("\x1B[8m", "\x1B[28m"), - strikethrough: f("\x1B[9m", "\x1B[29m"), - black: f("\x1B[30m", "\x1B[39m"), - red: f("\x1B[31m", "\x1B[39m"), - green: f("\x1B[32m", "\x1B[39m"), - yellow: f("\x1B[33m", "\x1B[39m"), - blue: f("\x1B[34m", "\x1B[39m"), - magenta: f("\x1B[35m", "\x1B[39m"), - cyan: f("\x1B[36m", "\x1B[39m"), - white: f("\x1B[37m", "\x1B[39m"), - gray: f("\x1B[90m", "\x1B[39m"), - bgBlack: f("\x1B[40m", "\x1B[49m"), - bgRed: f("\x1B[41m", "\x1B[49m"), - bgGreen: f("\x1B[42m", "\x1B[49m"), - bgYellow: f("\x1B[43m", "\x1B[49m"), - bgBlue: f("\x1B[44m", "\x1B[49m"), - bgMagenta: f("\x1B[45m", "\x1B[49m"), - bgCyan: f("\x1B[46m", "\x1B[49m"), - bgWhite: f("\x1B[47m", "\x1B[49m"), - blackBright: f("\x1B[90m", "\x1B[39m"), - redBright: f("\x1B[91m", "\x1B[39m"), - greenBright: f("\x1B[92m", "\x1B[39m"), - yellowBright: f("\x1B[93m", "\x1B[39m"), - blueBright: f("\x1B[94m", "\x1B[39m"), - magentaBright: f("\x1B[95m", "\x1B[39m"), - cyanBright: f("\x1B[96m", "\x1B[39m"), - whiteBright: f("\x1B[97m", "\x1B[39m"), - bgBlackBright: f("\x1B[100m", "\x1B[49m"), - bgRedBright: f("\x1B[101m", "\x1B[49m"), - bgGreenBright: f("\x1B[102m", "\x1B[49m"), - bgYellowBright: f("\x1B[103m", "\x1B[49m"), - bgBlueBright: f("\x1B[104m", "\x1B[49m"), - bgMagentaBright: f("\x1B[105m", "\x1B[49m"), - bgCyanBright: f("\x1B[106m", "\x1B[49m"), - bgWhiteBright: f("\x1B[107m", "\x1B[49m") - }; - }; - module.exports = createColors(); - module.exports.createColors = createColors; - } -}); - -// node_modules/sisteransi/src/index.js -var require_src = __commonJS({ - "node_modules/sisteransi/src/index.js"(exports, module) { - "use strict"; - var ESC = "\x1B"; - var CSI = `${ESC}[`; - var beep = "\x07"; - var cursor = { - to(x3, y2) { - if (!y2) return `${CSI}${x3 + 1}G`; - return `${CSI}${y2 + 1};${x3 + 1}H`; - }, - move(x3, y2) { - let ret = ""; - if (x3 < 0) ret += `${CSI}${-x3}D`; - else if (x3 > 0) ret += `${CSI}${x3}C`; - if (y2 < 0) ret += `${CSI}${-y2}A`; - else if (y2 > 0) ret += `${CSI}${y2}B`; - return ret; - }, - up: (count = 1) => `${CSI}${count}A`, - down: (count = 1) => `${CSI}${count}B`, - forward: (count = 1) => `${CSI}${count}C`, - backward: (count = 1) => `${CSI}${count}D`, - nextLine: (count = 1) => `${CSI}E`.repeat(count), - prevLine: (count = 1) => `${CSI}F`.repeat(count), - left: `${CSI}G`, - hide: `${CSI}?25l`, - show: `${CSI}?25h`, - save: `${ESC}7`, - restore: `${ESC}8` - }; - var scroll = { - up: (count = 1) => `${CSI}S`.repeat(count), - down: (count = 1) => `${CSI}T`.repeat(count) - }; - var erase = { - screen: `${CSI}2J`, - up: (count = 1) => `${CSI}1J`.repeat(count), - down: (count = 1) => `${CSI}J`.repeat(count), - line: `${CSI}2K`, - lineEnd: `${CSI}K`, - lineStart: `${CSI}1K`, - lines(count) { - let clear = ""; - for (let i = 0; i < count; i++) - clear += this.line + (i < count - 1 ? cursor.up() : ""); - if (count) - clear += cursor.left; - return clear; - } - }; - module.exports = { cursor, scroll, erase, beep }; - } -}); - -// node_modules/@clack/core/dist/index.mjs -var import_picocolors = __toESM(require_picocolors(), 1); -var import_sisteransi = __toESM(require_src(), 1); -import { stdout as R, stdin as q } from "node:process"; -import * as k from "node:readline"; -import ot from "node:readline"; -import { ReadStream as J } from "node:tty"; -function B(t, e2, s) { - if (!s.some((u) => !u.disabled)) return t; - const i = t + e2, r = Math.max(s.length - 1, 0), n = i < 0 ? r : i > r ? 0 : i; - return s[n].disabled ? B(n, e2 < 0 ? -1 : 1, s) : n; -} -var at = (t) => t === 161 || t === 164 || t === 167 || t === 168 || t === 170 || t === 173 || t === 174 || t >= 176 && t <= 180 || t >= 182 && t <= 186 || t >= 188 && t <= 191 || t === 198 || t === 208 || t === 215 || t === 216 || t >= 222 && t <= 225 || t === 230 || t >= 232 && t <= 234 || t === 236 || t === 237 || t === 240 || t === 242 || t === 243 || t >= 247 && t <= 250 || t === 252 || t === 254 || t === 257 || t === 273 || t === 275 || t === 283 || t === 294 || t === 295 || t === 299 || t >= 305 && t <= 307 || t === 312 || t >= 319 && t <= 322 || t === 324 || t >= 328 && t <= 331 || t === 333 || t === 338 || t === 339 || t === 358 || t === 359 || t === 363 || t === 462 || t === 464 || t === 466 || t === 468 || t === 470 || t === 472 || t === 474 || t === 476 || t === 593 || t === 609 || t === 708 || t === 711 || t >= 713 && t <= 715 || t === 717 || t === 720 || t >= 728 && t <= 731 || t === 733 || t === 735 || t >= 768 && t <= 879 || t >= 913 && t <= 929 || t >= 931 && t <= 937 || t >= 945 && t <= 961 || t >= 963 && t <= 969 || t === 1025 || t >= 1040 && t <= 1103 || t === 1105 || t === 8208 || t >= 8211 && t <= 8214 || t === 8216 || t === 8217 || t === 8220 || t === 8221 || t >= 8224 && t <= 8226 || t >= 8228 && t <= 8231 || t === 8240 || t === 8242 || t === 8243 || t === 8245 || t === 8251 || t === 8254 || t === 8308 || t === 8319 || t >= 8321 && t <= 8324 || t === 8364 || t === 8451 || t === 8453 || t === 8457 || t === 8467 || t === 8470 || t === 8481 || t === 8482 || t === 8486 || t === 8491 || t === 8531 || t === 8532 || t >= 8539 && t <= 8542 || t >= 8544 && t <= 8555 || t >= 8560 && t <= 8569 || t === 8585 || t >= 8592 && t <= 8601 || t === 8632 || t === 8633 || t === 8658 || t === 8660 || t === 8679 || t === 8704 || t === 8706 || t === 8707 || t === 8711 || t === 8712 || t === 8715 || t === 8719 || t === 8721 || t === 8725 || t === 8730 || t >= 8733 && t <= 8736 || t === 8739 || t === 8741 || t >= 8743 && t <= 8748 || t === 8750 || t >= 8756 && t <= 8759 || t === 8764 || t === 8765 || t === 8776 || t === 8780 || t === 8786 || t === 8800 || t === 8801 || t >= 8804 && t <= 8807 || t === 8810 || t === 8811 || t === 8814 || t === 8815 || t === 8834 || t === 8835 || t === 8838 || t === 8839 || t === 8853 || t === 8857 || t === 8869 || t === 8895 || t === 8978 || t >= 9312 && t <= 9449 || t >= 9451 && t <= 9547 || t >= 9552 && t <= 9587 || t >= 9600 && t <= 9615 || t >= 9618 && t <= 9621 || t === 9632 || t === 9633 || t >= 9635 && t <= 9641 || t === 9650 || t === 9651 || t === 9654 || t === 9655 || t === 9660 || t === 9661 || t === 9664 || t === 9665 || t >= 9670 && t <= 9672 || t === 9675 || t >= 9678 && t <= 9681 || t >= 9698 && t <= 9701 || t === 9711 || t === 9733 || t === 9734 || t === 9737 || t === 9742 || t === 9743 || t === 9756 || t === 9758 || t === 9792 || t === 9794 || t === 9824 || t === 9825 || t >= 9827 && t <= 9829 || t >= 9831 && t <= 9834 || t === 9836 || t === 9837 || t === 9839 || t === 9886 || t === 9887 || t === 9919 || t >= 9926 && t <= 9933 || t >= 9935 && t <= 9939 || t >= 9941 && t <= 9953 || t === 9955 || t === 9960 || t === 9961 || t >= 9963 && t <= 9969 || t === 9972 || t >= 9974 && t <= 9977 || t === 9979 || t === 9980 || t === 9982 || t === 9983 || t === 10045 || t >= 10102 && t <= 10111 || t >= 11094 && t <= 11097 || t >= 12872 && t <= 12879 || t >= 57344 && t <= 63743 || t >= 65024 && t <= 65039 || t === 65533 || t >= 127232 && t <= 127242 || t >= 127248 && t <= 127277 || t >= 127280 && t <= 127337 || t >= 127344 && t <= 127373 || t === 127375 || t === 127376 || t >= 127387 && t <= 127404 || t >= 917760 && t <= 917999 || t >= 983040 && t <= 1048573 || t >= 1048576 && t <= 1114109; -var lt = (t) => t === 12288 || t >= 65281 && t <= 65376 || t >= 65504 && t <= 65510; -var ht = (t) => t >= 4352 && t <= 4447 || t === 8986 || t === 8987 || t === 9001 || t === 9002 || t >= 9193 && t <= 9196 || t === 9200 || t === 9203 || t === 9725 || t === 9726 || t === 9748 || t === 9749 || t >= 9800 && t <= 9811 || t === 9855 || t === 9875 || t === 9889 || t === 9898 || t === 9899 || t === 9917 || t === 9918 || t === 9924 || t === 9925 || t === 9934 || t === 9940 || t === 9962 || t === 9970 || t === 9971 || t === 9973 || t === 9978 || t === 9981 || t === 9989 || t === 9994 || t === 9995 || t === 10024 || t === 10060 || t === 10062 || t >= 10067 && t <= 10069 || t === 10071 || t >= 10133 && t <= 10135 || t === 10160 || t === 10175 || t === 11035 || t === 11036 || t === 11088 || t === 11093 || t >= 11904 && t <= 11929 || t >= 11931 && t <= 12019 || t >= 12032 && t <= 12245 || t >= 12272 && t <= 12287 || t >= 12289 && t <= 12350 || t >= 12353 && t <= 12438 || t >= 12441 && t <= 12543 || t >= 12549 && t <= 12591 || t >= 12593 && t <= 12686 || t >= 12688 && t <= 12771 || t >= 12783 && t <= 12830 || t >= 12832 && t <= 12871 || t >= 12880 && t <= 19903 || t >= 19968 && t <= 42124 || t >= 42128 && t <= 42182 || t >= 43360 && t <= 43388 || t >= 44032 && t <= 55203 || t >= 63744 && t <= 64255 || t >= 65040 && t <= 65049 || t >= 65072 && t <= 65106 || t >= 65108 && t <= 65126 || t >= 65128 && t <= 65131 || t >= 94176 && t <= 94180 || t === 94192 || t === 94193 || t >= 94208 && t <= 100343 || t >= 100352 && t <= 101589 || t >= 101632 && t <= 101640 || t >= 110576 && t <= 110579 || t >= 110581 && t <= 110587 || t === 110589 || t === 110590 || t >= 110592 && t <= 110882 || t === 110898 || t >= 110928 && t <= 110930 || t === 110933 || t >= 110948 && t <= 110951 || t >= 110960 && t <= 111355 || t === 126980 || t === 127183 || t === 127374 || t >= 127377 && t <= 127386 || t >= 127488 && t <= 127490 || t >= 127504 && t <= 127547 || t >= 127552 && t <= 127560 || t === 127568 || t === 127569 || t >= 127584 && t <= 127589 || t >= 127744 && t <= 127776 || t >= 127789 && t <= 127797 || t >= 127799 && t <= 127868 || t >= 127870 && t <= 127891 || t >= 127904 && t <= 127946 || t >= 127951 && t <= 127955 || t >= 127968 && t <= 127984 || t === 127988 || t >= 127992 && t <= 128062 || t === 128064 || t >= 128066 && t <= 128252 || t >= 128255 && t <= 128317 || t >= 128331 && t <= 128334 || t >= 128336 && t <= 128359 || t === 128378 || t === 128405 || t === 128406 || t === 128420 || t >= 128507 && t <= 128591 || t >= 128640 && t <= 128709 || t === 128716 || t >= 128720 && t <= 128722 || t >= 128725 && t <= 128727 || t >= 128732 && t <= 128735 || t === 128747 || t === 128748 || t >= 128756 && t <= 128764 || t >= 128992 && t <= 129003 || t === 129008 || t >= 129292 && t <= 129338 || t >= 129340 && t <= 129349 || t >= 129351 && t <= 129535 || t >= 129648 && t <= 129660 || t >= 129664 && t <= 129672 || t >= 129680 && t <= 129725 || t >= 129727 && t <= 129733 || t >= 129742 && t <= 129755 || t >= 129760 && t <= 129768 || t >= 129776 && t <= 129784 || t >= 131072 && t <= 196605 || t >= 196608 && t <= 262141; -var O = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/y; -var y = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y; -var L = /\t{1,1000}/y; -var P = new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*", "yu"); -var M = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y; -var ct = new RegExp("\\p{M}+", "gu"); -var ft = { limit: 1 / 0, ellipsis: "" }; -var X = (t, e2 = {}, s = {}) => { - const i = e2.limit ?? 1 / 0, r = e2.ellipsis ?? "", n = e2?.ellipsisWidth ?? (r ? X(r, ft, s).width : 0), u = s.ansiWidth ?? 0, a = s.controlWidth ?? 0, l = s.tabWidth ?? 8, E = s.ambiguousWidth ?? 1, g = s.emojiWidth ?? 2, m = s.fullWidthWidth ?? 2, A = s.regularWidth ?? 1, V2 = s.wideWidth ?? 2; - let h = 0, o = 0, p = t.length, v = 0, F = false, d2 = p, b = Math.max(0, i - n), C2 = 0, w = 0, c = 0, f = 0; - t: for (; ; ) { - if (w > C2 || o >= p && o > h) { - const ut = t.slice(C2, w) || t.slice(h, o); - v = 0; - for (const Y of ut.replaceAll(ct, "")) { - const $ = Y.codePointAt(0) || 0; - if (lt($) ? f = m : ht($) ? f = V2 : E !== A && at($) ? f = E : f = A, c + f > b && (d2 = Math.min(d2, Math.max(C2, h) + v)), c + f > i) { - F = true; - break t; - } - v += Y.length, c += f; - } - C2 = w = 0; - } - if (o >= p) break; - if (M.lastIndex = o, M.test(t)) { - if (v = M.lastIndex - o, f = v * A, c + f > b && (d2 = Math.min(d2, o + Math.floor((b - c) / A))), c + f > i) { - F = true; - break; - } - c += f, C2 = h, w = o, o = h = M.lastIndex; - continue; - } - if (O.lastIndex = o, O.test(t)) { - if (c + u > b && (d2 = Math.min(d2, o)), c + u > i) { - F = true; - break; - } - c += u, C2 = h, w = o, o = h = O.lastIndex; - continue; - } - if (y.lastIndex = o, y.test(t)) { - if (v = y.lastIndex - o, f = v * a, c + f > b && (d2 = Math.min(d2, o + Math.floor((b - c) / a))), c + f > i) { - F = true; - break; - } - c += f, C2 = h, w = o, o = h = y.lastIndex; - continue; - } - if (L.lastIndex = o, L.test(t)) { - if (v = L.lastIndex - o, f = v * l, c + f > b && (d2 = Math.min(d2, o + Math.floor((b - c) / l))), c + f > i) { - F = true; - break; - } - c += f, C2 = h, w = o, o = h = L.lastIndex; - continue; - } - if (P.lastIndex = o, P.test(t)) { - if (c + g > b && (d2 = Math.min(d2, o)), c + g > i) { - F = true; - break; - } - c += g, C2 = h, w = o, o = h = P.lastIndex; - continue; - } - o += 1; - } - return { width: F ? b : c, index: F ? d2 : p, truncated: F, ellipsed: F && i >= n }; -}; -var pt = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; -var S = (t, e2 = {}) => X(t, pt, e2).width; -var W = "\x1B"; -var Z = "\x9B"; -var Ft = 39; -var j = "\x07"; -var Q = "["; -var dt = "]"; -var tt = "m"; -var U = `${dt}8;;`; -var et = new RegExp(`(?:\\${Q}(?\\d+)m|\\${U}(?.*)${j})`, "y"); -var mt = (t) => { - if (t >= 30 && t <= 37 || t >= 90 && t <= 97) return 39; - if (t >= 40 && t <= 47 || t >= 100 && t <= 107) return 49; - if (t === 1 || t === 2) return 22; - if (t === 3) return 23; - if (t === 4) return 24; - if (t === 7) return 27; - if (t === 8) return 28; - if (t === 9) return 29; - if (t === 0) return 0; -}; -var st = (t) => `${W}${Q}${t}${tt}`; -var it = (t) => `${W}${U}${t}${j}`; -var gt = (t) => t.map((e2) => S(e2)); -var G = (t, e2, s) => { - const i = e2[Symbol.iterator](); - let r = false, n = false, u = t.at(-1), a = u === void 0 ? 0 : S(u), l = i.next(), E = i.next(), g = 0; - for (; !l.done; ) { - const m = l.value, A = S(m); - a + A <= s ? t[t.length - 1] += m : (t.push(m), a = 0), (m === W || m === Z) && (r = true, n = e2.startsWith(U, g + 1)), r ? n ? m === j && (r = false, n = false) : m === tt && (r = false) : (a += A, a === s && !E.done && (t.push(""), a = 0)), l = E, E = i.next(), g += m.length; - } - u = t.at(-1), !a && u !== void 0 && u.length > 0 && t.length > 1 && (t[t.length - 2] += t.pop()); -}; -var vt = (t) => { - const e2 = t.split(" "); - let s = e2.length; - for (; s > 0 && !(S(e2[s - 1]) > 0); ) s--; - return s === e2.length ? t : e2.slice(0, s).join(" ") + e2.slice(s).join(""); -}; -var Et = (t, e2, s = {}) => { - if (s.trim !== false && t.trim() === "") return ""; - let i = "", r, n; - const u = t.split(" "), a = gt(u); - let l = [""]; - for (const [h, o] of u.entries()) { - s.trim !== false && (l[l.length - 1] = (l.at(-1) ?? "").trimStart()); - let p = S(l.at(-1) ?? ""); - if (h !== 0 && (p >= e2 && (s.wordWrap === false || s.trim === false) && (l.push(""), p = 0), (p > 0 || s.trim === false) && (l[l.length - 1] += " ", p++)), s.hard && a[h] > e2) { - const v = e2 - p, F = 1 + Math.floor((a[h] - v - 1) / e2); - Math.floor((a[h] - 1) / e2) < F && l.push(""), G(l, o, e2); - continue; - } - if (p + a[h] > e2 && p > 0 && a[h] > 0) { - if (s.wordWrap === false && p < e2) { - G(l, o, e2); - continue; - } - l.push(""); - } - if (p + a[h] > e2 && s.wordWrap === false) { - G(l, o, e2); - continue; - } - l[l.length - 1] += o; - } - s.trim !== false && (l = l.map((h) => vt(h))); - const E = l.join(` -`), g = E[Symbol.iterator](); - let m = g.next(), A = g.next(), V2 = 0; - for (; !m.done; ) { - const h = m.value, o = A.value; - if (i += h, h === W || h === Z) { - et.lastIndex = V2 + 1; - const F = et.exec(E)?.groups; - if (F?.code !== void 0) { - const d2 = Number.parseFloat(F.code); - r = d2 === Ft ? void 0 : d2; - } else F?.uri !== void 0 && (n = F.uri.length === 0 ? void 0 : F.uri); - } - const p = r ? mt(r) : void 0; - o === ` -` ? (n && (i += it("")), r && p && (i += st(p))) : h === ` -` && (r && p && (i += st(r)), n && (i += it(n))), V2 += h.length, m = A, A = g.next(); - } - return i; -}; -function K(t, e2, s) { - return String(t).normalize().replaceAll(`\r -`, ` -`).split(` -`).map((i) => Et(i, e2, s)).join(` -`); -} -var At = ["up", "down", "left", "right", "space", "enter", "cancel"]; -var _ = { actions: new Set(At), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]), messages: { cancel: "Canceled", error: "Something went wrong" }, withGuide: true }; -function H(t, e2) { - if (typeof t == "string") return _.aliases.get(t) === e2; - for (const s of t) if (s !== void 0 && H(s, e2)) return true; - return false; -} -function _t(t, e2) { - if (t === e2) return; - const s = t.split(` -`), i = e2.split(` -`), r = Math.max(s.length, i.length), n = []; - for (let u = 0; u < r; u++) s[u] !== i[u] && n.push(u); - return { lines: n, numLinesBefore: s.length, numLinesAfter: i.length, numLines: r }; -} -var bt = globalThis.process.platform.startsWith("win"); -var z = Symbol("clack:cancel"); -function Ct(t) { - return t === z; -} -function T(t, e2) { - const s = t; - s.isTTY && s.setRawMode(e2); -} -function Bt({ input: t = q, output: e2 = R, overwrite: s = true, hideCursor: i = true } = {}) { - const r = k.createInterface({ input: t, output: e2, prompt: "", tabSize: 1 }); - k.emitKeypressEvents(t, r), t instanceof J && t.isTTY && t.setRawMode(true); - const n = (u, { name: a, sequence: l }) => { - const E = String(u); - if (H([E, a, l], "cancel")) { - i && e2.write(import_sisteransi.cursor.show), process.exit(0); - return; - } - if (!s) return; - const g = a === "return" ? 0 : -1, m = a === "return" ? -1 : 0; - k.moveCursor(e2, g, m, () => { - k.clearLine(e2, 1, () => { - t.once("keypress", n); - }); - }); - }; - return i && e2.write(import_sisteransi.cursor.hide), t.once("keypress", n), () => { - t.off("keypress", n), i && e2.write(import_sisteransi.cursor.show), t instanceof J && t.isTTY && !bt && t.setRawMode(false), r.terminal = false, r.close(); - }; -} -var rt = (t) => "columns" in t && typeof t.columns == "number" ? t.columns : 80; -var nt = (t) => "rows" in t && typeof t.rows == "number" ? t.rows : 20; -function xt(t, e2, s, i = s) { - const r = rt(t ?? R); - return K(e2, r - s.length, { hard: true, trim: false }).split(` -`).map((n, u) => `${u === 0 ? i : s}${n}`).join(` -`); -} -var x = class { - input; - output; - _abortSignal; - rl; - opts; - _render; - _track = false; - _prevFrame = ""; - _subscribers = /* @__PURE__ */ new Map(); - _cursor = 0; - state = "initial"; - error = ""; - value; - userInput = ""; - constructor(e2, s = true) { - const { input: i = q, output: r = R, render: n, signal: u, ...a } = e2; - this.opts = a, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = n.bind(this), this._track = s, this._abortSignal = u, this.input = i, this.output = r; - } - unsubscribe() { - this._subscribers.clear(); - } - setSubscriber(e2, s) { - const i = this._subscribers.get(e2) ?? []; - i.push(s), this._subscribers.set(e2, i); - } - on(e2, s) { - this.setSubscriber(e2, { cb: s }); - } - once(e2, s) { - this.setSubscriber(e2, { cb: s, once: true }); - } - emit(e2, ...s) { - const i = this._subscribers.get(e2) ?? [], r = []; - for (const n of i) n.cb(...s), n.once && r.push(() => i.splice(i.indexOf(n), 1)); - for (const n of r) n(); - } - prompt() { - return new Promise((e2) => { - if (this._abortSignal) { - if (this._abortSignal.aborted) return this.state = "cancel", this.close(), e2(z); - this._abortSignal.addEventListener("abort", () => { - this.state = "cancel", this.close(); - }, { once: true }); - } - this.rl = ot.createInterface({ input: this.input, tabSize: 2, prompt: "", escapeCodeTimeout: 50, terminal: true }), this.rl.prompt(), this.opts.initialUserInput !== void 0 && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), T(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => { - this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), T(this.input, false), e2(this.value); - }), this.once("cancel", () => { - this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), T(this.input, false), e2(z); - }); - }); - } - _isActionKey(e2, s) { - return e2 === " "; - } - _setValue(e2) { - this.value = e2, this.emit("value", this.value); - } - _setUserInput(e2, s) { - this.userInput = e2 ?? "", this.emit("userInput", this.userInput), s && this._track && this.rl && (this.rl.write(this.userInput), this._cursor = this.rl.cursor); - } - _clearUserInput() { - this.rl?.write(null, { ctrl: true, name: "u" }), this._setUserInput(""); - } - onKeypress(e2, s) { - if (this._track && s.name !== "return" && (s.name && this._isActionKey(e2, s) && this.rl?.write(null, { ctrl: true, name: "h" }), this._cursor = this.rl?.cursor ?? 0, this._setUserInput(this.rl?.line)), this.state === "error" && (this.state = "active"), s?.name && (!this._track && _.aliases.has(s.name) && this.emit("cursor", _.aliases.get(s.name)), _.actions.has(s.name) && this.emit("cursor", s.name)), e2 && (e2.toLowerCase() === "y" || e2.toLowerCase() === "n") && this.emit("confirm", e2.toLowerCase() === "y"), this.emit("key", e2?.toLowerCase(), s), s?.name === "return") { - if (this.opts.validate) { - const i = this.opts.validate(this.value); - i && (this.error = i instanceof Error ? i.message : i, this.state = "error", this.rl?.write(this.userInput)); - } - this.state !== "error" && (this.state = "submit"); - } - H([e2, s?.name, s?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close(); - } - close() { - this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(` -`), T(this.input, false), this.rl?.close(), this.rl = void 0, this.emit(`${this.state}`, this.value), this.unsubscribe(); - } - restoreCursor() { - const e2 = K(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split(` -`).length - 1; - this.output.write(import_sisteransi.cursor.move(-999, e2 * -1)); - } - render() { - const e2 = K(this._render(this) ?? "", process.stdout.columns, { hard: true, trim: false }); - if (e2 !== this._prevFrame) { - if (this.state === "initial") this.output.write(import_sisteransi.cursor.hide); - else { - const s = _t(this._prevFrame, e2), i = nt(this.output); - if (this.restoreCursor(), s) { - const r = Math.max(0, s.numLinesAfter - i), n = Math.max(0, s.numLinesBefore - i); - let u = s.lines.find((a) => a >= r); - if (u === void 0) { - this._prevFrame = e2; - return; - } - if (s.lines.length === 1) { - this.output.write(import_sisteransi.cursor.move(0, u - n)), this.output.write(import_sisteransi.erase.lines(1)); - const a = e2.split(` -`); - this.output.write(a[u]), this._prevFrame = e2, this.output.write(import_sisteransi.cursor.move(0, a.length - u - 1)); - return; - } else if (s.lines.length > 1) { - if (r < n) u = r; - else { - const l = u - n; - l > 0 && this.output.write(import_sisteransi.cursor.move(0, l)); - } - this.output.write(import_sisteransi.erase.down()); - const a = e2.split(` -`).slice(u); - this.output.write(a.join(` -`)), this._prevFrame = e2; - return; - } - } - this.output.write(import_sisteransi.erase.down()); - } - this.output.write(e2), this.state === "initial" && (this.state = "active"), this._prevFrame = e2; - } - } -}; -var kt = class extends x { - get cursor() { - return this.value ? 0 : 1; - } - get _value() { - return this.cursor === 0; - } - constructor(e2) { - super(e2, false), this.value = !!e2.initialValue, this.on("userInput", () => { - this.value = this._value; - }), this.on("confirm", (s) => { - this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = s, this.state = "submit", this.close(); - }), this.on("cursor", () => { - this.value = !this.value; - }); - } -}; -var Lt = class extends x { - options; - cursor = 0; - get _value() { - return this.options[this.cursor].value; - } - get _enabledOptions() { - return this.options.filter((e2) => e2.disabled !== true); - } - toggleAll() { - const e2 = this._enabledOptions, s = this.value !== void 0 && this.value.length === e2.length; - this.value = s ? [] : e2.map((i) => i.value); - } - toggleInvert() { - const e2 = this.value; - if (!e2) return; - const s = this._enabledOptions.filter((i) => !e2.includes(i.value)); - this.value = s.map((i) => i.value); - } - toggleValue() { - this.value === void 0 && (this.value = []); - const e2 = this.value.includes(this._value); - this.value = e2 ? this.value.filter((s) => s !== this._value) : [...this.value, this._value]; - } - constructor(e2) { - super(e2, false), this.options = e2.options, this.value = [...e2.initialValues ?? []]; - const s = Math.max(this.options.findIndex(({ value: i }) => i === e2.cursorAt), 0); - this.cursor = this.options[s].disabled ? B(s, 1, this.options) : s, this.on("key", (i) => { - i === "a" && this.toggleAll(), i === "i" && this.toggleInvert(); - }), this.on("cursor", (i) => { - switch (i) { - case "left": - case "up": - this.cursor = B(this.cursor, -1, this.options); - break; - case "down": - case "right": - this.cursor = B(this.cursor, 1, this.options); - break; - case "space": - this.toggleValue(); - break; - } - }); - } -}; -var Mt = class extends x { - _mask = "\u2022"; - get cursor() { - return this._cursor; - } - get masked() { - return this.userInput.replaceAll(/./g, this._mask); - } - get userInputWithCursor() { - if (this.state === "submit" || this.state === "cancel") return this.masked; - const e2 = this.userInput; - if (this.cursor >= e2.length) return `${this.masked}${import_picocolors.default.inverse(import_picocolors.default.hidden("_"))}`; - const s = this.masked, i = s.slice(0, this.cursor), r = s.slice(this.cursor); - return `${i}${import_picocolors.default.inverse(r[0])}${r.slice(1)}`; - } - clear() { - this._clearUserInput(); - } - constructor({ mask: e2, ...s }) { - super(s), this._mask = e2 ?? "\u2022", this.on("userInput", (i) => { - this._setValue(i); - }); - } -}; -var Wt = class extends x { - options; - cursor = 0; - get _selectedValue() { - return this.options[this.cursor]; - } - changeValue() { - this.value = this._selectedValue.value; - } - constructor(e2) { - super(e2, false), this.options = e2.options; - const s = this.options.findIndex(({ value: r }) => r === e2.initialValue), i = s === -1 ? 0 : s; - this.cursor = this.options[i].disabled ? B(i, 1, this.options) : i, this.changeValue(), this.on("cursor", (r) => { - switch (r) { - case "left": - case "up": - this.cursor = B(this.cursor, -1, this.options); - break; - case "down": - case "right": - this.cursor = B(this.cursor, 1, this.options); - break; - } - this.changeValue(); - }); - } -}; -var $t = class extends x { - get userInputWithCursor() { - if (this.state === "submit") return this.userInput; - const e2 = this.userInput; - if (this.cursor >= e2.length) return `${this.userInput}\u2588`; - const s = e2.slice(0, this.cursor), [i, ...r] = e2.slice(this.cursor); - return `${s}${import_picocolors.default.inverse(i)}${r.join("")}`; - } - get cursor() { - return this._cursor; - } - constructor(e2) { - super({ ...e2, initialUserInput: e2.initialUserInput ?? e2.initialValue }), this.on("userInput", (s) => { - this._setValue(s); - }), this.on("finalize", () => { - this.value || (this.value = e2.defaultValue), this.value === void 0 && (this.value = ""); - }); - } -}; - -// node_modules/@clack/prompts/dist/index.mjs -var import_picocolors2 = __toESM(require_picocolors(), 1); -import N2 from "node:process"; -var import_sisteransi2 = __toESM(require_src(), 1); -function me() { - return N2.platform !== "win32" ? N2.env.TERM !== "linux" : !!N2.env.CI || !!N2.env.WT_SESSION || !!N2.env.TERMINUS_SUBLIME || N2.env.ConEmuTask === "{cmd::Cmder}" || N2.env.TERM_PROGRAM === "Terminus-Sublime" || N2.env.TERM_PROGRAM === "vscode" || N2.env.TERM === "xterm-256color" || N2.env.TERM === "alacritty" || N2.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; -} -var et2 = me(); -var ct2 = () => process.env.CI === "true"; -var C = (t, r) => et2 ? t : r; -var Rt = C("\u25C6", "*"); -var dt2 = C("\u25A0", "x"); -var $t2 = C("\u25B2", "x"); -var V = C("\u25C7", "o"); -var ht2 = C("\u250C", "T"); -var d = C("\u2502", "|"); -var x2 = C("\u2514", "\u2014"); -var Ot = C("\u2510", "T"); -var Pt = C("\u2518", "\u2014"); -var Q2 = C("\u25CF", ">"); -var H2 = C("\u25CB", " "); -var st2 = C("\u25FB", "[\u2022]"); -var U2 = C("\u25FC", "[+]"); -var q2 = C("\u25FB", "[ ]"); -var Nt = C("\u25AA", "\u2022"); -var rt2 = C("\u2500", "-"); -var mt2 = C("\u256E", "+"); -var Wt2 = C("\u251C", "+"); -var pt2 = C("\u256F", "+"); -var gt2 = C("\u2570", "+"); -var Lt2 = C("\u256D", "+"); -var ft2 = C("\u25CF", "\u2022"); -var Ft2 = C("\u25C6", "*"); -var yt2 = C("\u25B2", "!"); -var Et2 = C("\u25A0", "x"); -var W2 = (t) => { - switch (t) { - case "initial": - case "active": - return import_picocolors2.default.cyan(Rt); - case "cancel": - return import_picocolors2.default.red(dt2); - case "error": - return import_picocolors2.default.yellow($t2); - case "submit": - return import_picocolors2.default.green(V); - } -}; -var vt2 = (t) => { - switch (t) { - case "initial": - case "active": - return import_picocolors2.default.cyan(d); - case "cancel": - return import_picocolors2.default.red(d); - case "error": - return import_picocolors2.default.yellow(d); - case "submit": - return import_picocolors2.default.green(d); - } -}; -var pe = (t) => t === 161 || t === 164 || t === 167 || t === 168 || t === 170 || t === 173 || t === 174 || t >= 176 && t <= 180 || t >= 182 && t <= 186 || t >= 188 && t <= 191 || t === 198 || t === 208 || t === 215 || t === 216 || t >= 222 && t <= 225 || t === 230 || t >= 232 && t <= 234 || t === 236 || t === 237 || t === 240 || t === 242 || t === 243 || t >= 247 && t <= 250 || t === 252 || t === 254 || t === 257 || t === 273 || t === 275 || t === 283 || t === 294 || t === 295 || t === 299 || t >= 305 && t <= 307 || t === 312 || t >= 319 && t <= 322 || t === 324 || t >= 328 && t <= 331 || t === 333 || t === 338 || t === 339 || t === 358 || t === 359 || t === 363 || t === 462 || t === 464 || t === 466 || t === 468 || t === 470 || t === 472 || t === 474 || t === 476 || t === 593 || t === 609 || t === 708 || t === 711 || t >= 713 && t <= 715 || t === 717 || t === 720 || t >= 728 && t <= 731 || t === 733 || t === 735 || t >= 768 && t <= 879 || t >= 913 && t <= 929 || t >= 931 && t <= 937 || t >= 945 && t <= 961 || t >= 963 && t <= 969 || t === 1025 || t >= 1040 && t <= 1103 || t === 1105 || t === 8208 || t >= 8211 && t <= 8214 || t === 8216 || t === 8217 || t === 8220 || t === 8221 || t >= 8224 && t <= 8226 || t >= 8228 && t <= 8231 || t === 8240 || t === 8242 || t === 8243 || t === 8245 || t === 8251 || t === 8254 || t === 8308 || t === 8319 || t >= 8321 && t <= 8324 || t === 8364 || t === 8451 || t === 8453 || t === 8457 || t === 8467 || t === 8470 || t === 8481 || t === 8482 || t === 8486 || t === 8491 || t === 8531 || t === 8532 || t >= 8539 && t <= 8542 || t >= 8544 && t <= 8555 || t >= 8560 && t <= 8569 || t === 8585 || t >= 8592 && t <= 8601 || t === 8632 || t === 8633 || t === 8658 || t === 8660 || t === 8679 || t === 8704 || t === 8706 || t === 8707 || t === 8711 || t === 8712 || t === 8715 || t === 8719 || t === 8721 || t === 8725 || t === 8730 || t >= 8733 && t <= 8736 || t === 8739 || t === 8741 || t >= 8743 && t <= 8748 || t === 8750 || t >= 8756 && t <= 8759 || t === 8764 || t === 8765 || t === 8776 || t === 8780 || t === 8786 || t === 8800 || t === 8801 || t >= 8804 && t <= 8807 || t === 8810 || t === 8811 || t === 8814 || t === 8815 || t === 8834 || t === 8835 || t === 8838 || t === 8839 || t === 8853 || t === 8857 || t === 8869 || t === 8895 || t === 8978 || t >= 9312 && t <= 9449 || t >= 9451 && t <= 9547 || t >= 9552 && t <= 9587 || t >= 9600 && t <= 9615 || t >= 9618 && t <= 9621 || t === 9632 || t === 9633 || t >= 9635 && t <= 9641 || t === 9650 || t === 9651 || t === 9654 || t === 9655 || t === 9660 || t === 9661 || t === 9664 || t === 9665 || t >= 9670 && t <= 9672 || t === 9675 || t >= 9678 && t <= 9681 || t >= 9698 && t <= 9701 || t === 9711 || t === 9733 || t === 9734 || t === 9737 || t === 9742 || t === 9743 || t === 9756 || t === 9758 || t === 9792 || t === 9794 || t === 9824 || t === 9825 || t >= 9827 && t <= 9829 || t >= 9831 && t <= 9834 || t === 9836 || t === 9837 || t === 9839 || t === 9886 || t === 9887 || t === 9919 || t >= 9926 && t <= 9933 || t >= 9935 && t <= 9939 || t >= 9941 && t <= 9953 || t === 9955 || t === 9960 || t === 9961 || t >= 9963 && t <= 9969 || t === 9972 || t >= 9974 && t <= 9977 || t === 9979 || t === 9980 || t === 9982 || t === 9983 || t === 10045 || t >= 10102 && t <= 10111 || t >= 11094 && t <= 11097 || t >= 12872 && t <= 12879 || t >= 57344 && t <= 63743 || t >= 65024 && t <= 65039 || t === 65533 || t >= 127232 && t <= 127242 || t >= 127248 && t <= 127277 || t >= 127280 && t <= 127337 || t >= 127344 && t <= 127373 || t === 127375 || t === 127376 || t >= 127387 && t <= 127404 || t >= 917760 && t <= 917999 || t >= 983040 && t <= 1048573 || t >= 1048576 && t <= 1114109; -var ge = (t) => t === 12288 || t >= 65281 && t <= 65376 || t >= 65504 && t <= 65510; -var fe = (t) => t >= 4352 && t <= 4447 || t === 8986 || t === 8987 || t === 9001 || t === 9002 || t >= 9193 && t <= 9196 || t === 9200 || t === 9203 || t === 9725 || t === 9726 || t === 9748 || t === 9749 || t >= 9800 && t <= 9811 || t === 9855 || t === 9875 || t === 9889 || t === 9898 || t === 9899 || t === 9917 || t === 9918 || t === 9924 || t === 9925 || t === 9934 || t === 9940 || t === 9962 || t === 9970 || t === 9971 || t === 9973 || t === 9978 || t === 9981 || t === 9989 || t === 9994 || t === 9995 || t === 10024 || t === 10060 || t === 10062 || t >= 10067 && t <= 10069 || t === 10071 || t >= 10133 && t <= 10135 || t === 10160 || t === 10175 || t === 11035 || t === 11036 || t === 11088 || t === 11093 || t >= 11904 && t <= 11929 || t >= 11931 && t <= 12019 || t >= 12032 && t <= 12245 || t >= 12272 && t <= 12287 || t >= 12289 && t <= 12350 || t >= 12353 && t <= 12438 || t >= 12441 && t <= 12543 || t >= 12549 && t <= 12591 || t >= 12593 && t <= 12686 || t >= 12688 && t <= 12771 || t >= 12783 && t <= 12830 || t >= 12832 && t <= 12871 || t >= 12880 && t <= 19903 || t >= 19968 && t <= 42124 || t >= 42128 && t <= 42182 || t >= 43360 && t <= 43388 || t >= 44032 && t <= 55203 || t >= 63744 && t <= 64255 || t >= 65040 && t <= 65049 || t >= 65072 && t <= 65106 || t >= 65108 && t <= 65126 || t >= 65128 && t <= 65131 || t >= 94176 && t <= 94180 || t === 94192 || t === 94193 || t >= 94208 && t <= 100343 || t >= 100352 && t <= 101589 || t >= 101632 && t <= 101640 || t >= 110576 && t <= 110579 || t >= 110581 && t <= 110587 || t === 110589 || t === 110590 || t >= 110592 && t <= 110882 || t === 110898 || t >= 110928 && t <= 110930 || t === 110933 || t >= 110948 && t <= 110951 || t >= 110960 && t <= 111355 || t === 126980 || t === 127183 || t === 127374 || t >= 127377 && t <= 127386 || t >= 127488 && t <= 127490 || t >= 127504 && t <= 127547 || t >= 127552 && t <= 127560 || t === 127568 || t === 127569 || t >= 127584 && t <= 127589 || t >= 127744 && t <= 127776 || t >= 127789 && t <= 127797 || t >= 127799 && t <= 127868 || t >= 127870 && t <= 127891 || t >= 127904 && t <= 127946 || t >= 127951 && t <= 127955 || t >= 127968 && t <= 127984 || t === 127988 || t >= 127992 && t <= 128062 || t === 128064 || t >= 128066 && t <= 128252 || t >= 128255 && t <= 128317 || t >= 128331 && t <= 128334 || t >= 128336 && t <= 128359 || t === 128378 || t === 128405 || t === 128406 || t === 128420 || t >= 128507 && t <= 128591 || t >= 128640 && t <= 128709 || t === 128716 || t >= 128720 && t <= 128722 || t >= 128725 && t <= 128727 || t >= 128732 && t <= 128735 || t === 128747 || t === 128748 || t >= 128756 && t <= 128764 || t >= 128992 && t <= 129003 || t === 129008 || t >= 129292 && t <= 129338 || t >= 129340 && t <= 129349 || t >= 129351 && t <= 129535 || t >= 129648 && t <= 129660 || t >= 129664 && t <= 129672 || t >= 129680 && t <= 129725 || t >= 129727 && t <= 129733 || t >= 129742 && t <= 129755 || t >= 129760 && t <= 129768 || t >= 129776 && t <= 129784 || t >= 131072 && t <= 196605 || t >= 196608 && t <= 262141; -var At2 = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/y; -var it2 = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y; -var nt2 = /\t{1,1000}/y; -var wt = new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*", "yu"); -var at2 = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y; -var Fe = new RegExp("\\p{M}+", "gu"); -var ye = { limit: 1 / 0, ellipsis: "" }; -var jt = (t, r = {}, s = {}) => { - const i = r.limit ?? 1 / 0, a = r.ellipsis ?? "", o = r?.ellipsisWidth ?? (a ? jt(a, ye, s).width : 0), u = s.ansiWidth ?? 0, l = s.controlWidth ?? 0, n = s.tabWidth ?? 8, c = s.ambiguousWidth ?? 1, g = s.emojiWidth ?? 2, F = s.fullWidthWidth ?? 2, p = s.regularWidth ?? 1, E = s.wideWidth ?? 2; - let $ = 0, m = 0, h = t.length, y2 = 0, f = false, v = h, S2 = Math.max(0, i - o), I2 = 0, B2 = 0, A = 0, w = 0; - t: for (; ; ) { - if (B2 > I2 || m >= h && m > $) { - const _2 = t.slice(I2, B2) || t.slice($, m); - y2 = 0; - for (const D2 of _2.replaceAll(Fe, "")) { - const T2 = D2.codePointAt(0) || 0; - if (ge(T2) ? w = F : fe(T2) ? w = E : c !== p && pe(T2) ? w = c : w = p, A + w > S2 && (v = Math.min(v, Math.max(I2, $) + y2)), A + w > i) { - f = true; - break t; - } - y2 += D2.length, A += w; - } - I2 = B2 = 0; - } - if (m >= h) break; - if (at2.lastIndex = m, at2.test(t)) { - if (y2 = at2.lastIndex - m, w = y2 * p, A + w > S2 && (v = Math.min(v, m + Math.floor((S2 - A) / p))), A + w > i) { - f = true; - break; - } - A += w, I2 = $, B2 = m, m = $ = at2.lastIndex; - continue; - } - if (At2.lastIndex = m, At2.test(t)) { - if (A + u > S2 && (v = Math.min(v, m)), A + u > i) { - f = true; - break; - } - A += u, I2 = $, B2 = m, m = $ = At2.lastIndex; - continue; - } - if (it2.lastIndex = m, it2.test(t)) { - if (y2 = it2.lastIndex - m, w = y2 * l, A + w > S2 && (v = Math.min(v, m + Math.floor((S2 - A) / l))), A + w > i) { - f = true; - break; - } - A += w, I2 = $, B2 = m, m = $ = it2.lastIndex; - continue; - } - if (nt2.lastIndex = m, nt2.test(t)) { - if (y2 = nt2.lastIndex - m, w = y2 * n, A + w > S2 && (v = Math.min(v, m + Math.floor((S2 - A) / n))), A + w > i) { - f = true; - break; - } - A += w, I2 = $, B2 = m, m = $ = nt2.lastIndex; - continue; - } - if (wt.lastIndex = m, wt.test(t)) { - if (A + g > S2 && (v = Math.min(v, m)), A + g > i) { - f = true; - break; - } - A += g, I2 = $, B2 = m, m = $ = wt.lastIndex; - continue; - } - m += 1; - } - return { width: f ? S2 : A, index: f ? v : h, truncated: f, ellipsed: f && i >= o }; -}; -var Ee = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; -var M2 = (t, r = {}) => jt(t, Ee, r).width; -var ot2 = "\x1B"; -var Gt = "\x9B"; -var ve = 39; -var Ct2 = "\x07"; -var kt2 = "["; -var Ae = "]"; -var Vt2 = "m"; -var St = `${Ae}8;;`; -var Ht = new RegExp(`(?:\\${kt2}(?\\d+)m|\\${St}(?.*)${Ct2})`, "y"); -var we = (t) => { - if (t >= 30 && t <= 37 || t >= 90 && t <= 97) return 39; - if (t >= 40 && t <= 47 || t >= 100 && t <= 107) return 49; - if (t === 1 || t === 2) return 22; - if (t === 3) return 23; - if (t === 4) return 24; - if (t === 7) return 27; - if (t === 8) return 28; - if (t === 9) return 29; - if (t === 0) return 0; -}; -var Ut = (t) => `${ot2}${kt2}${t}${Vt2}`; -var Kt = (t) => `${ot2}${St}${t}${Ct2}`; -var Ce = (t) => t.map((r) => M2(r)); -var It2 = (t, r, s) => { - const i = r[Symbol.iterator](); - let a = false, o = false, u = t.at(-1), l = u === void 0 ? 0 : M2(u), n = i.next(), c = i.next(), g = 0; - for (; !n.done; ) { - const F = n.value, p = M2(F); - l + p <= s ? t[t.length - 1] += F : (t.push(F), l = 0), (F === ot2 || F === Gt) && (a = true, o = r.startsWith(St, g + 1)), a ? o ? F === Ct2 && (a = false, o = false) : F === Vt2 && (a = false) : (l += p, l === s && !c.done && (t.push(""), l = 0)), n = c, c = i.next(), g += F.length; - } - u = t.at(-1), !l && u !== void 0 && u.length > 0 && t.length > 1 && (t[t.length - 2] += t.pop()); -}; -var Se = (t) => { - const r = t.split(" "); - let s = r.length; - for (; s > 0 && !(M2(r[s - 1]) > 0); ) s--; - return s === r.length ? t : r.slice(0, s).join(" ") + r.slice(s).join(""); -}; -var Ie = (t, r, s = {}) => { - if (s.trim !== false && t.trim() === "") return ""; - let i = "", a, o; - const u = t.split(" "), l = Ce(u); - let n = [""]; - for (const [$, m] of u.entries()) { - s.trim !== false && (n[n.length - 1] = (n.at(-1) ?? "").trimStart()); - let h = M2(n.at(-1) ?? ""); - if ($ !== 0 && (h >= r && (s.wordWrap === false || s.trim === false) && (n.push(""), h = 0), (h > 0 || s.trim === false) && (n[n.length - 1] += " ", h++)), s.hard && l[$] > r) { - const y2 = r - h, f = 1 + Math.floor((l[$] - y2 - 1) / r); - Math.floor((l[$] - 1) / r) < f && n.push(""), It2(n, m, r); - continue; - } - if (h + l[$] > r && h > 0 && l[$] > 0) { - if (s.wordWrap === false && h < r) { - It2(n, m, r); - continue; - } - n.push(""); - } - if (h + l[$] > r && s.wordWrap === false) { - It2(n, m, r); - continue; - } - n[n.length - 1] += m; - } - s.trim !== false && (n = n.map(($) => Se($))); - const c = n.join(` -`), g = c[Symbol.iterator](); - let F = g.next(), p = g.next(), E = 0; - for (; !F.done; ) { - const $ = F.value, m = p.value; - if (i += $, $ === ot2 || $ === Gt) { - Ht.lastIndex = E + 1; - const f = Ht.exec(c)?.groups; - if (f?.code !== void 0) { - const v = Number.parseFloat(f.code); - a = v === ve ? void 0 : v; - } else f?.uri !== void 0 && (o = f.uri.length === 0 ? void 0 : f.uri); - } - const h = a ? we(a) : void 0; - m === ` -` ? (o && (i += Kt("")), a && h && (i += Ut(h))) : $ === ` -` && (a && h && (i += Ut(a)), o && (i += Kt(o))), E += $.length, F = p, p = g.next(); - } - return i; -}; -function J2(t, r, s) { - return String(t).normalize().replaceAll(`\r -`, ` -`).split(` -`).map((i) => Ie(i, r, s)).join(` -`); -} -var be = (t, r, s, i, a) => { - let o = r, u = 0; - for (let l = s; l < i; l++) { - const n = t[l]; - if (o = o - n.length, u++, o <= a) break; - } - return { lineCount: o, removals: u }; -}; -var X2 = (t) => { - const { cursor: r, options: s, style: i } = t, a = t.output ?? process.stdout, o = rt(a), u = t.columnPadding ?? 0, l = t.rowPadding ?? 4, n = o - u, c = nt(a), g = import_picocolors2.default.dim("..."), F = t.maxItems ?? Number.POSITIVE_INFINITY, p = Math.max(c - l, 0), E = Math.max(Math.min(F, p), 5); - let $ = 0; - r >= E - 3 && ($ = Math.max(Math.min(r - E + 3, s.length - E), 0)); - let m = E < s.length && $ > 0, h = E < s.length && $ + E < s.length; - const y2 = Math.min($ + E, s.length), f = []; - let v = 0; - m && v++, h && v++; - const S2 = $ + (m ? 1 : 0), I2 = y2 - (h ? 1 : 0); - for (let A = S2; A < I2; A++) { - const w = J2(i(s[A], A === r), n, { hard: true, trim: false }).split(` -`); - f.push(w), v += w.length; - } - if (v > p) { - let A = 0, w = 0, _2 = v; - const D2 = r - S2, T2 = (Y, L2) => be(f, _2, Y, L2, p); - m ? ({ lineCount: _2, removals: A } = T2(0, D2), _2 > p && ({ lineCount: _2, removals: w } = T2(D2 + 1, f.length))) : ({ lineCount: _2, removals: w } = T2(D2 + 1, f.length), _2 > p && ({ lineCount: _2, removals: A } = T2(0, D2))), A > 0 && (m = true, f.splice(0, A)), w > 0 && (h = true, f.splice(f.length - w, w)); - } - const B2 = []; - m && B2.push(g); - for (const A of f) for (const w of A) B2.push(w); - return h && B2.push(g), B2; -}; -var Re = (t) => { - const r = t.active ?? "Yes", s = t.inactive ?? "No"; - return new kt({ active: r, inactive: s, signal: t.signal, input: t.input, output: t.output, initialValue: t.initialValue ?? true, render() { - const i = t.withGuide ?? _.withGuide, a = `${i ? `${import_picocolors2.default.gray(d)} -` : ""}${W2(this.state)} ${t.message} -`, o = this.value ? r : s; - switch (this.state) { - case "submit": { - const u = i ? `${import_picocolors2.default.gray(d)} ` : ""; - return `${a}${u}${import_picocolors2.default.dim(o)}`; - } - case "cancel": { - const u = i ? `${import_picocolors2.default.gray(d)} ` : ""; - return `${a}${u}${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o))}${i ? ` -${import_picocolors2.default.gray(d)}` : ""}`; - } - default: { - const u = i ? `${import_picocolors2.default.cyan(d)} ` : "", l = i ? import_picocolors2.default.cyan(x2) : ""; - return `${a}${u}${this.value ? `${import_picocolors2.default.green(Q2)} ${r}` : `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(r)}`}${t.vertical ? i ? ` -${import_picocolors2.default.cyan(d)} ` : ` -` : ` ${import_picocolors2.default.dim("/")} `}${this.value ? `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.green(Q2)} ${s}`} -${l} -`; - } - } - } }).prompt(); -}; -var R2 = { message: (t = [], { symbol: r = import_picocolors2.default.gray(d), secondarySymbol: s = import_picocolors2.default.gray(d), output: i = process.stdout, spacing: a = 1, withGuide: o } = {}) => { - const u = [], l = o ?? _.withGuide, n = l ? s : "", c = l ? `${r} ` : "", g = l ? `${s} ` : ""; - for (let p = 0; p < a; p++) u.push(n); - const F = Array.isArray(t) ? t : t.split(` -`); - if (F.length > 0) { - const [p, ...E] = F; - p.length > 0 ? u.push(`${c}${p}`) : u.push(l ? r : ""); - for (const $ of E) $.length > 0 ? u.push(`${g}${$}`) : u.push(l ? s : ""); - } - i.write(`${u.join(` -`)} -`); -}, info: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.blue(ft2) }); -}, success: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.green(Ft2) }); -}, step: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.green(V) }); -}, warn: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.yellow(yt2) }); -}, warning: (t, r) => { - R2.warn(t, r); -}, error: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.red(Et2) }); -} }; -var Ne = (t = "", r) => { - (r?.output ?? process.stdout).write(`${import_picocolors2.default.gray(x2)} ${import_picocolors2.default.red(t)} - -`); -}; -var We = (t = "", r) => { - (r?.output ?? process.stdout).write(`${import_picocolors2.default.gray(ht2)} ${t} -`); -}; -var Le = (t = "", r) => { - (r?.output ?? process.stdout).write(`${import_picocolors2.default.gray(d)} -${import_picocolors2.default.gray(x2)} ${t} - -`); -}; -var Z2 = (t, r) => t.split(` -`).map((s) => r(s)).join(` -`); -var je = (t) => { - const r = (i, a) => { - const o = i.label ?? String(i.value); - return a === "disabled" ? `${import_picocolors2.default.gray(q2)} ${Z2(o, (u) => import_picocolors2.default.strikethrough(import_picocolors2.default.gray(u)))}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint ?? "disabled"})`)}` : ""}` : a === "active" ? `${import_picocolors2.default.cyan(st2)} ${o}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : a === "selected" ? `${import_picocolors2.default.green(U2)} ${Z2(o, import_picocolors2.default.dim)}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : a === "cancelled" ? `${Z2(o, (u) => import_picocolors2.default.strikethrough(import_picocolors2.default.dim(u)))}` : a === "active-selected" ? `${import_picocolors2.default.green(U2)} ${o}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : a === "submitted" ? `${Z2(o, import_picocolors2.default.dim)}` : `${import_picocolors2.default.dim(q2)} ${Z2(o, import_picocolors2.default.dim)}`; - }, s = t.required ?? true; - return new Lt({ options: t.options, signal: t.signal, input: t.input, output: t.output, initialValues: t.initialValues, required: s, cursorAt: t.cursorAt, validate(i) { - if (s && (i === void 0 || i.length === 0)) return `Please select at least one option. -${import_picocolors2.default.reset(import_picocolors2.default.dim(`Press ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" space ")))} to select, ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" enter ")))} to submit`))}`; - }, render() { - const i = xt(t.output, t.message, `${vt2(this.state)} `, `${W2(this.state)} `), a = `${import_picocolors2.default.gray(d)} -${i} -`, o = this.value ?? [], u = (l, n) => { - if (l.disabled) return r(l, "disabled"); - const c = o.includes(l.value); - return n && c ? r(l, "active-selected") : c ? r(l, "selected") : r(l, n ? "active" : "inactive"); - }; - switch (this.state) { - case "submit": { - const l = this.options.filter(({ value: c }) => o.includes(c)).map((c) => r(c, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none"), n = xt(t.output, l, `${import_picocolors2.default.gray(d)} `); - return `${a}${n}`; - } - case "cancel": { - const l = this.options.filter(({ value: c }) => o.includes(c)).map((c) => r(c, "cancelled")).join(import_picocolors2.default.dim(", ")); - if (l.trim() === "") return `${a}${import_picocolors2.default.gray(d)}`; - const n = xt(t.output, l, `${import_picocolors2.default.gray(d)} `); - return `${a}${n} -${import_picocolors2.default.gray(d)}`; - } - case "error": { - const l = `${import_picocolors2.default.yellow(d)} `, n = this.error.split(` -`).map((F, p) => p === 0 ? `${import_picocolors2.default.yellow(x2)} ${import_picocolors2.default.yellow(F)}` : ` ${F}`).join(` -`), c = a.split(` -`).length, g = n.split(` -`).length + 1; - return `${a}${l}${X2({ output: t.output, options: this.options, cursor: this.cursor, maxItems: t.maxItems, columnPadding: l.length, rowPadding: c + g, style: u }).join(` -${l}`)} -${n} -`; - } - default: { - const l = `${import_picocolors2.default.cyan(d)} `, n = a.split(` -`).length; - return `${a}${l}${X2({ output: t.output, options: this.options, cursor: this.cursor, maxItems: t.maxItems, columnPadding: l.length, rowPadding: n + 2, style: u }).join(` -${l}`)} -${import_picocolors2.default.cyan(x2)} -`; - } - } - } }).prompt(); -}; -var Ge = (t) => import_picocolors2.default.dim(t); -var ke = (t, r, s) => { - const i = { hard: true, trim: false }, a = J2(t, r, i).split(` -`), o = a.reduce((n, c) => Math.max(M2(c), n), 0), u = a.map(s).reduce((n, c) => Math.max(M2(c), n), 0), l = r - (u - o); - return J2(t, l, i); -}; -var Ve = (t = "", r = "", s) => { - const i = s?.output ?? N2.stdout, a = s?.withGuide ?? _.withGuide, o = s?.format ?? Ge, u = ["", ...ke(t, rt(i) - 6, o).split(` -`).map(o), ""], l = M2(r), n = Math.max(u.reduce((p, E) => { - const $ = M2(E); - return $ > p ? $ : p; - }, 0), l) + 2, c = u.map((p) => `${import_picocolors2.default.gray(d)} ${p}${" ".repeat(n - M2(p))}${import_picocolors2.default.gray(d)}`).join(` -`), g = a ? `${import_picocolors2.default.gray(d)} -` : "", F = a ? Wt2 : gt2; - i.write(`${g}${import_picocolors2.default.green(V)} ${import_picocolors2.default.reset(r)} ${import_picocolors2.default.gray(rt2.repeat(Math.max(n - l - 1, 1)) + mt2)} -${c} -${import_picocolors2.default.gray(F + rt2.repeat(n + 2) + pt2)} -`); -}; -var He = (t) => new Mt({ validate: t.validate, mask: t.mask ?? Nt, signal: t.signal, input: t.input, output: t.output, render() { - const r = t.withGuide ?? _.withGuide, s = `${r ? `${import_picocolors2.default.gray(d)} -` : ""}${W2(this.state)} ${t.message} -`, i = this.userInputWithCursor, a = this.masked; - switch (this.state) { - case "error": { - const o = r ? `${import_picocolors2.default.yellow(d)} ` : "", u = r ? `${import_picocolors2.default.yellow(x2)} ` : "", l = a ?? ""; - return t.clearOnError && this.clear(), `${s.trim()} -${o}${l} -${u}${import_picocolors2.default.yellow(this.error)} -`; - } - case "submit": { - const o = r ? `${import_picocolors2.default.gray(d)} ` : "", u = a ? import_picocolors2.default.dim(a) : ""; - return `${s}${o}${u}`; - } - case "cancel": { - const o = r ? `${import_picocolors2.default.gray(d)} ` : "", u = a ? import_picocolors2.default.strikethrough(import_picocolors2.default.dim(a)) : ""; - return `${s}${o}${u}${a && r ? ` -${import_picocolors2.default.gray(d)}` : ""}`; - } - default: { - const o = r ? `${import_picocolors2.default.cyan(d)} ` : "", u = r ? import_picocolors2.default.cyan(x2) : ""; - return `${s}${o}${i} -${u} -`; - } - } -} }).prompt(); -var Ke = import_picocolors2.default.magenta; -var bt2 = ({ indicator: t = "dots", onCancel: r, output: s = process.stdout, cancelMessage: i, errorMessage: a, frames: o = et2 ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: u = et2 ? 80 : 120, signal: l, ...n } = {}) => { - const c = ct2(); - let g, F, p = false, E = false, $ = "", m, h = performance.now(); - const y2 = rt(s), f = n?.styleFrame ?? Ke, v = (b) => { - const O2 = b > 1 ? a ?? _.messages.error : i ?? _.messages.cancel; - E = b === 1, p && (L2(O2, b), E && typeof r == "function" && r()); - }, S2 = () => v(2), I2 = () => v(1), B2 = () => { - process.on("uncaughtExceptionMonitor", S2), process.on("unhandledRejection", S2), process.on("SIGINT", I2), process.on("SIGTERM", I2), process.on("exit", v), l && l.addEventListener("abort", I2); - }, A = () => { - process.removeListener("uncaughtExceptionMonitor", S2), process.removeListener("unhandledRejection", S2), process.removeListener("SIGINT", I2), process.removeListener("SIGTERM", I2), process.removeListener("exit", v), l && l.removeEventListener("abort", I2); - }, w = () => { - if (m === void 0) return; - c && s.write(` -`); - const b = J2(m, y2, { hard: true, trim: false }).split(` -`); - b.length > 1 && s.write(import_sisteransi2.cursor.up(b.length - 1)), s.write(import_sisteransi2.cursor.to(0)), s.write(import_sisteransi2.erase.down()); - }, _2 = (b) => b.replace(/\.+$/, ""), D2 = (b) => { - const O2 = (performance.now() - b) / 1e3, j2 = Math.floor(O2 / 60), G2 = Math.floor(O2 % 60); - return j2 > 0 ? `[${j2}m ${G2}s]` : `[${G2}s]`; - }, T2 = n.withGuide ?? _.withGuide, Y = (b = "") => { - p = true, g = Bt({ output: s }), $ = _2(b), h = performance.now(), T2 && s.write(`${import_picocolors2.default.gray(d)} -`); - let O2 = 0, j2 = 0; - B2(), F = setInterval(() => { - if (c && $ === m) return; - w(), m = $; - const G2 = f(o[O2]); - let tt2; - if (c) tt2 = `${G2} ${$}...`; - else if (t === "timer") tt2 = `${G2} ${$} ${D2(h)}`; - else { - const te = ".".repeat(Math.floor(j2)).slice(0, 3); - tt2 = `${G2} ${$}${te}`; - } - const Zt = J2(tt2, y2, { hard: true, trim: false }); - s.write(Zt), O2 = O2 + 1 < o.length ? O2 + 1 : 0, j2 = j2 < 4 ? j2 + 0.125 : 0; - }, u); - }, L2 = (b = "", O2 = 0, j2 = false) => { - if (!p) return; - p = false, clearInterval(F), w(); - const G2 = O2 === 0 ? import_picocolors2.default.green(V) : O2 === 1 ? import_picocolors2.default.red(dt2) : import_picocolors2.default.red($t2); - $ = b ?? $, j2 || (t === "timer" ? s.write(`${G2} ${$} ${D2(h)} -`) : s.write(`${G2} ${$} -`)), A(), g(); - }; - return { start: Y, stop: (b = "") => L2(b, 0), message: (b = "") => { - $ = _2(b ?? $); - }, cancel: (b = "") => L2(b, 1), error: (b = "") => L2(b, 2), clear: () => L2("", 0, true), get isCancelled() { - return E; - } }; -}; -var zt = { light: C("\u2500", "-"), heavy: C("\u2501", "="), block: C("\u2588", "#") }; -var lt2 = (t, r) => t.includes(` -`) ? t.split(` -`).map((s) => r(s)).join(` -`) : r(t); -var Je = (t) => { - const r = (s, i) => { - const a = s.label ?? String(s.value); - switch (i) { - case "disabled": - return `${import_picocolors2.default.gray(H2)} ${lt2(a, import_picocolors2.default.gray)}${s.hint ? ` ${import_picocolors2.default.dim(`(${s.hint ?? "disabled"})`)}` : ""}`; - case "selected": - return `${lt2(a, import_picocolors2.default.dim)}`; - case "active": - return `${import_picocolors2.default.green(Q2)} ${a}${s.hint ? ` ${import_picocolors2.default.dim(`(${s.hint})`)}` : ""}`; - case "cancelled": - return `${lt2(a, (o) => import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o)))}`; - default: - return `${import_picocolors2.default.dim(H2)} ${lt2(a, import_picocolors2.default.dim)}`; - } - }; - return new Wt({ options: t.options, signal: t.signal, input: t.input, output: t.output, initialValue: t.initialValue, render() { - const s = t.withGuide ?? _.withGuide, i = `${W2(this.state)} `, a = `${vt2(this.state)} `, o = xt(t.output, t.message, a, i), u = `${s ? `${import_picocolors2.default.gray(d)} -` : ""}${o} -`; - switch (this.state) { - case "submit": { - const l = s ? `${import_picocolors2.default.gray(d)} ` : "", n = xt(t.output, r(this.options[this.cursor], "selected"), l); - return `${u}${n}`; - } - case "cancel": { - const l = s ? `${import_picocolors2.default.gray(d)} ` : "", n = xt(t.output, r(this.options[this.cursor], "cancelled"), l); - return `${u}${n}${s ? ` -${import_picocolors2.default.gray(d)}` : ""}`; - } - default: { - const l = s ? `${import_picocolors2.default.cyan(d)} ` : "", n = s ? import_picocolors2.default.cyan(x2) : "", c = u.split(` -`).length, g = s ? 2 : 1; - return `${u}${l}${X2({ output: t.output, cursor: this.cursor, options: this.options, maxItems: t.maxItems, columnPadding: l.length, rowPadding: c + g, style: (F, p) => r(F, F.disabled ? "disabled" : p ? "active" : "inactive") }).join(` -${l}`)} -${n} -`; - } - } - } }).prompt(); -}; -var Qt = `${import_picocolors2.default.gray(d)} `; -var Ye = async (t, r) => { - for (const s of t) { - if (s.enabled === false) continue; - const i = bt2(r); - i.start(s.title); - const a = await s.task(i.message); - i.stop(a || s.title); - } -}; -var Ze = (t) => new $t({ validate: t.validate, placeholder: t.placeholder, defaultValue: t.defaultValue, initialValue: t.initialValue, output: t.output, signal: t.signal, input: t.input, render() { - const r = t?.withGuide ?? _.withGuide, s = `${`${r ? `${import_picocolors2.default.gray(d)} -` : ""}${W2(this.state)} `}${t.message} -`, i = t.placeholder ? import_picocolors2.default.inverse(t.placeholder[0]) + import_picocolors2.default.dim(t.placeholder.slice(1)) : import_picocolors2.default.inverse(import_picocolors2.default.hidden("_")), a = this.userInput ? this.userInputWithCursor : i, o = this.value ?? ""; - switch (this.state) { - case "error": { - const u = this.error ? ` ${import_picocolors2.default.yellow(this.error)}` : "", l = r ? `${import_picocolors2.default.yellow(d)} ` : "", n = r ? import_picocolors2.default.yellow(x2) : ""; - return `${s.trim()} -${l}${a} -${n}${u} -`; - } - case "submit": { - const u = o ? ` ${import_picocolors2.default.dim(o)}` : "", l = r ? import_picocolors2.default.gray(d) : ""; - return `${s}${l}${u}`; - } - case "cancel": { - const u = o ? ` ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o))}` : "", l = r ? import_picocolors2.default.gray(d) : ""; - return `${s}${l}${u}${o.trim() ? ` -${l}` : ""}`; - } - default: { - const u = r ? `${import_picocolors2.default.cyan(d)} ` : "", l = r ? import_picocolors2.default.cyan(x2) : ""; - return `${s}${u}${a} -${l} -`; - } - } -} }).prompt(); - -// src/steps/welcome.ts -var import_picocolors3 = __toESM(require_picocolors(), 1); -import { existsSync } from "fs"; - -// src/utils/system.ts -import { execSync } from "child_process"; -import { homedir } from "os"; -import { join } from "path"; -function detectOS() { - switch (process.platform) { - case "darwin": - return "macos"; - case "win32": - return "windows"; - default: - return "linux"; - } -} -function commandExists(command) { - try { - execSync(`which ${command}`, { stdio: "pipe" }); - return true; - } catch { - return false; - } -} -function runCommand(command, args = []) { - try { - const fullCommand = [command, ...args].join(" "); - const stdout = execSync(fullCommand, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); - return { stdout: stdout.trim(), stderr: "", exitCode: 0 }; - } catch (error) { - return { - stdout: error.stdout?.toString().trim() ?? "", - stderr: error.stderr?.toString().trim() ?? "", - exitCode: error.status ?? 1 - }; - } -} -function expandHome(filepath) { - if (filepath.startsWith("~")) { - return join(homedir(), filepath.slice(1)); - } - return filepath; -} - -// src/steps/welcome.ts -async function runWelcome() { - We(import_picocolors3.default.bgCyan(import_picocolors3.default.black(" claude-mem installer "))); - R2.info(`Version: 1.0.0`); - R2.info(`Platform: ${process.platform} (${process.arch})`); - const settingsExist = existsSync(expandHome("~/.claude-mem/settings.json")); - const pluginExist = existsSync(expandHome("~/.claude/plugins/marketplaces/thedotmack/")); - const alreadyInstalled = settingsExist && pluginExist; - if (alreadyInstalled) { - R2.warn("Existing claude-mem installation detected."); - } - const installMode = await Je({ - message: "What would you like to do?", - options: alreadyInstalled ? [ - { value: "upgrade", label: "Upgrade", hint: "update to latest version" }, - { value: "configure", label: "Configure", hint: "change settings only" }, - { value: "fresh", label: "Fresh Install", hint: "reinstall from scratch" } - ] : [ - { value: "fresh", label: "Fresh Install", hint: "recommended" }, - { value: "configure", label: "Configure Only", hint: "set up settings without installing" } - ] - }); - if (Ct(installMode)) { - Ne("Installation cancelled."); - process.exit(0); - } - return installMode; -} - -// src/steps/dependencies.ts -var import_picocolors4 = __toESM(require_picocolors(), 1); - -// src/utils/dependencies.ts -import { existsSync as existsSync2 } from "fs"; -import { execSync as execSync2 } from "child_process"; -function findBinary(name, extraPaths = []) { - if (commandExists(name)) { - const result = runCommand("which", [name]); - const versionResult = runCommand(name, ["--version"]); - return { - found: true, - path: result.stdout, - version: parseVersion(versionResult.stdout) || parseVersion(versionResult.stderr) - }; - } - for (const extraPath of extraPaths) { - const fullPath = expandHome(extraPath); - if (existsSync2(fullPath)) { - const versionResult = runCommand(fullPath, ["--version"]); - return { - found: true, - path: fullPath, - version: parseVersion(versionResult.stdout) || parseVersion(versionResult.stderr) - }; - } - } - return { found: false, path: null, version: null }; -} -function parseVersion(output) { - if (!output) return null; - const match = output.match(/(\d+\.\d+(\.\d+)?)/); - return match ? match[1] : null; -} -function compareVersions(current, minimum) { - const currentParts = current.split(".").map(Number); - const minimumParts = minimum.split(".").map(Number); - for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i++) { - const a = currentParts[i] || 0; - const b = minimumParts[i] || 0; - if (a > b) return true; - if (a < b) return false; - } - return true; -} -function installBun() { - const os = detectOS(); - if (os === "windows") { - execSync2('powershell -c "irm bun.sh/install.ps1 | iex"', { stdio: "inherit" }); - } else { - execSync2("curl -fsSL https://bun.sh/install | bash", { stdio: "inherit" }); - } -} -function installUv() { - const os = detectOS(); - if (os === "windows") { - execSync2('powershell -c "irm https://astral.sh/uv/install.ps1 | iex"', { stdio: "inherit" }); - } else { - execSync2("curl -fsSL https://astral.sh/uv/install.sh | sh", { stdio: "inherit" }); - } -} - -// src/steps/dependencies.ts -var BUN_EXTRA_PATHS = ["~/.bun/bin/bun", "/usr/local/bin/bun", "/opt/homebrew/bin/bun"]; -var UV_EXTRA_PATHS = ["~/.local/bin/uv", "~/.cargo/bin/uv"]; -async function runDependencyChecks() { - const status = { - nodeOk: false, - gitOk: false, - bunOk: false, - uvOk: false, - bunPath: null, - uvPath: null - }; - await Ye([ - { - title: "Checking Node.js", - task: async () => { - const version = process.version.slice(1); - if (compareVersions(version, "18.0.0")) { - status.nodeOk = true; - return `Node.js ${process.version} ${import_picocolors4.default.green("\u2713")}`; - } - return `Node.js ${process.version} \u2014 requires >= 18.0.0 ${import_picocolors4.default.red("\u2717")}`; - } - }, - { - title: "Checking git", - task: async () => { - const info = findBinary("git"); - if (info.found) { - status.gitOk = true; - return `git ${info.version ?? ""} ${import_picocolors4.default.green("\u2713")}`; - } - return `git not found ${import_picocolors4.default.red("\u2717")}`; - } - }, - { - title: "Checking Bun", - task: async () => { - const info = findBinary("bun", BUN_EXTRA_PATHS); - if (info.found && info.version && compareVersions(info.version, "1.1.14")) { - status.bunOk = true; - status.bunPath = info.path; - return `Bun ${info.version} ${import_picocolors4.default.green("\u2713")}`; - } - if (info.found && info.version) { - return `Bun ${info.version} \u2014 requires >= 1.1.14 ${import_picocolors4.default.yellow("\u26A0")}`; - } - return `Bun not found ${import_picocolors4.default.yellow("\u26A0")}`; - } - }, - { - title: "Checking uv", - task: async () => { - const info = findBinary("uv", UV_EXTRA_PATHS); - if (info.found) { - status.uvOk = true; - status.uvPath = info.path; - return `uv ${info.version ?? ""} ${import_picocolors4.default.green("\u2713")}`; - } - return `uv not found ${import_picocolors4.default.yellow("\u26A0")}`; - } - } - ]); - if (!status.gitOk) { - const os = detectOS(); - R2.error("git is required but not found."); - if (os === "macos") { - R2.info("Install with: xcode-select --install"); - } else if (os === "linux") { - R2.info("Install with: sudo apt install git (or your distro equivalent)"); - } else { - R2.info("Download from: https://git-scm.com/downloads"); - } - Ne("Please install git and try again."); - process.exit(1); - } - if (!status.nodeOk) { - R2.error(`Node.js >= 18.0.0 is required. Current: ${process.version}`); - Ne("Please upgrade Node.js and try again."); - process.exit(1); - } - if (!status.bunOk) { - const shouldInstall = await Re({ - message: "Bun is required but not found. Install it now?", - initialValue: true - }); - if (Ct(shouldInstall)) { - Ne("Installation cancelled."); - process.exit(0); - } - if (shouldInstall) { - const s = bt2(); - s.start("Installing Bun..."); - try { - installBun(); - const recheck = findBinary("bun", BUN_EXTRA_PATHS); - if (recheck.found) { - status.bunOk = true; - status.bunPath = recheck.path; - s.stop(`Bun installed ${import_picocolors4.default.green("\u2713")}`); - } else { - s.stop(`Bun installed but not found in PATH. You may need to restart your shell.`); - } - } catch { - s.stop(`Bun installation failed. Install manually: curl -fsSL https://bun.sh/install | bash`); - } - } else { - R2.warn("Bun is required for claude-mem. Install manually: curl -fsSL https://bun.sh/install | bash"); - Ne("Cannot continue without Bun."); - process.exit(1); - } - } - if (!status.uvOk) { - const shouldInstall = await Re({ - message: "uv (Python package manager) is recommended for Chroma. Install it now?", - initialValue: true - }); - if (Ct(shouldInstall)) { - Ne("Installation cancelled."); - process.exit(0); - } - if (shouldInstall) { - const s = bt2(); - s.start("Installing uv..."); - try { - installUv(); - const recheck = findBinary("uv", UV_EXTRA_PATHS); - if (recheck.found) { - status.uvOk = true; - status.uvPath = recheck.path; - s.stop(`uv installed ${import_picocolors4.default.green("\u2713")}`); - } else { - s.stop("uv installed but not found in PATH. You may need to restart your shell."); - } - } catch { - s.stop("uv installation failed. Install manually: curl -fsSL https://astral.sh/uv/install.sh | sh"); - } - } else { - R2.warn("Skipping uv \u2014 Chroma vector search will not be available."); - } - } - return status; -} - -// src/steps/ide-selection.ts -async function runIdeSelection() { - const result = await je({ - message: "Which IDEs do you use?", - options: [ - { value: "claude-code", label: "Claude Code", hint: "recommended" }, - { value: "cursor", label: "Cursor" } - // Windsurf coming soon - not yet selectable - ], - initialValues: ["claude-code"], - required: true - }); - if (Ct(result)) { - Ne("Installation cancelled."); - process.exit(0); - } - const selectedIDEs = result; - if (selectedIDEs.includes("claude-code")) { - R2.info("Claude Code: Plugin will be registered via marketplace."); - } - if (selectedIDEs.includes("cursor")) { - R2.info("Cursor: Hooks will be configured for your projects."); - } - return selectedIDEs; -} - -// src/steps/provider.ts -async function runProviderConfiguration() { - const provider = await Je({ - message: "Which AI provider should claude-mem use for memory compression?", - options: [ - { value: "claude", label: "Claude", hint: "uses your Claude subscription" }, - { value: "gemini", label: "Gemini", hint: "free tier available" }, - { value: "openrouter", label: "OpenRouter", hint: "free models available" } - ] - }); - if (Ct(provider)) { - Ne("Installation cancelled."); - process.exit(0); - } - const config = { provider }; - if (provider === "claude") { - const authMethod = await Je({ - message: "How should Claude authenticate?", - options: [ - { value: "cli", label: "CLI (Max Plan subscription)", hint: "no API key needed" }, - { value: "api", label: "API Key", hint: "uses Anthropic API credits" } - ] - }); - if (Ct(authMethod)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.claudeAuthMethod = authMethod; - if (authMethod === "api") { - const apiKey = await He({ - message: "Enter your Anthropic API key:", - validate: (value) => { - if (!value || value.trim().length === 0) return "API key is required"; - if (!value.startsWith("sk-ant-")) return "Anthropic API keys start with sk-ant-"; - } - }); - if (Ct(apiKey)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.apiKey = apiKey; - } - } - if (provider === "gemini") { - const apiKey = await He({ - message: "Enter your Gemini API key:", - validate: (value) => { - if (!value || value.trim().length === 0) return "API key is required"; - } - }); - if (Ct(apiKey)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.apiKey = apiKey; - const model = await Je({ - message: "Which Gemini model?", - options: [ - { value: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite", hint: "fastest, highest free RPM" }, - { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", hint: "balanced" }, - { value: "gemini-3-flash-preview", label: "Gemini 3 Flash Preview", hint: "latest" } - ] - }); - if (Ct(model)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.model = model; - const rateLimiting = await Re({ - message: "Enable rate limiting? (recommended for free tier)", - initialValue: true - }); - if (Ct(rateLimiting)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.rateLimitingEnabled = rateLimiting; - } - if (provider === "openrouter") { - const apiKey = await He({ - message: "Enter your OpenRouter API key:", - validate: (value) => { - if (!value || value.trim().length === 0) return "API key is required"; - } - }); - if (Ct(apiKey)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.apiKey = apiKey; - const model = await Ze({ - message: "Which OpenRouter model?", - defaultValue: "xiaomi/mimo-v2-flash:free", - placeholder: "xiaomi/mimo-v2-flash:free" - }); - if (Ct(model)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.model = model; - } - return config; -} - -// src/steps/settings.ts -var import_picocolors5 = __toESM(require_picocolors(), 1); -async function runSettingsConfiguration() { - const useDefaults = await Re({ - message: "Use default settings? (recommended for most users)", - initialValue: true - }); - if (Ct(useDefaults)) { - Ne("Installation cancelled."); - process.exit(0); - } - if (useDefaults) { - return { - workerPort: "37777", - dataDir: "~/.claude-mem", - contextObservations: "50", - logLevel: "INFO", - pythonVersion: "3.13", - chromaEnabled: true, - chromaMode: "local" - }; - } - const workerPort = await Ze({ - message: "Worker service port:", - defaultValue: "37777", - placeholder: "37777", - validate: (value = "") => { - const port = parseInt(value, 10); - if (isNaN(port) || port < 1024 || port > 65535) { - return "Port must be between 1024 and 65535"; - } - } - }); - if (Ct(workerPort)) { - Ne("Installation cancelled."); - process.exit(0); - } - const dataDir = await Ze({ - message: "Data directory:", - defaultValue: "~/.claude-mem", - placeholder: "~/.claude-mem" - }); - if (Ct(dataDir)) { - Ne("Installation cancelled."); - process.exit(0); - } - const contextObservations = await Ze({ - message: "Number of context observations per session:", - defaultValue: "50", - placeholder: "50", - validate: (value = "") => { - const num = parseInt(value, 10); - if (isNaN(num) || num < 1 || num > 200) { - return "Must be between 1 and 200"; - } - } - }); - if (Ct(contextObservations)) { - Ne("Installation cancelled."); - process.exit(0); - } - const logLevel = await Je({ - message: "Log level:", - options: [ - { value: "DEBUG", label: "DEBUG", hint: "verbose" }, - { value: "INFO", label: "INFO", hint: "default" }, - { value: "WARN", label: "WARN" }, - { value: "ERROR", label: "ERROR", hint: "errors only" } - ], - initialValue: "INFO" - }); - if (Ct(logLevel)) { - Ne("Installation cancelled."); - process.exit(0); - } - const pythonVersion = await Ze({ - message: "Python version (for Chroma):", - defaultValue: "3.13", - placeholder: "3.13" - }); - if (Ct(pythonVersion)) { - Ne("Installation cancelled."); - process.exit(0); - } - const chromaEnabled = await Re({ - message: "Enable Chroma vector search?", - initialValue: true - }); - if (Ct(chromaEnabled)) { - Ne("Installation cancelled."); - process.exit(0); - } - let chromaMode; - let chromaHost; - let chromaPort; - let chromaSsl; - if (chromaEnabled) { - const mode = await Je({ - message: "Chroma mode:", - options: [ - { value: "local", label: "Local", hint: "starts local Chroma server" }, - { value: "remote", label: "Remote", hint: "connect to existing server" } - ] - }); - if (Ct(mode)) { - Ne("Installation cancelled."); - process.exit(0); - } - chromaMode = mode; - if (mode === "remote") { - const host = await Ze({ - message: "Chroma host:", - defaultValue: "127.0.0.1", - placeholder: "127.0.0.1" - }); - if (Ct(host)) { - Ne("Installation cancelled."); - process.exit(0); - } - chromaHost = host; - const port = await Ze({ - message: "Chroma port:", - defaultValue: "8000", - placeholder: "8000", - validate: (value = "") => { - const portNum = parseInt(value, 10); - if (isNaN(portNum) || portNum < 1 || portNum > 65535) return "Port must be between 1 and 65535"; - } - }); - if (Ct(port)) { - Ne("Installation cancelled."); - process.exit(0); - } - chromaPort = port; - const ssl = await Re({ - message: "Use SSL for Chroma connection?", - initialValue: false - }); - if (Ct(ssl)) { - Ne("Installation cancelled."); - process.exit(0); - } - chromaSsl = ssl; - } - } - const config = { - workerPort, - dataDir, - contextObservations, - logLevel, - pythonVersion, - chromaEnabled, - chromaMode, - chromaHost, - chromaPort, - chromaSsl - }; - const summaryLines = [ - `Worker port: ${import_picocolors5.default.cyan(workerPort)}`, - `Data directory: ${import_picocolors5.default.cyan(dataDir)}`, - `Context observations: ${import_picocolors5.default.cyan(contextObservations)}`, - `Log level: ${import_picocolors5.default.cyan(logLevel)}`, - `Python version: ${import_picocolors5.default.cyan(pythonVersion)}`, - `Chroma: ${chromaEnabled ? import_picocolors5.default.green("enabled") : import_picocolors5.default.dim("disabled")}` - ]; - if (chromaEnabled && chromaMode) { - summaryLines.push(`Chroma mode: ${import_picocolors5.default.cyan(chromaMode)}`); - } - Ve(summaryLines.join("\n"), "Settings Summary"); - return config; -} - -// src/utils/settings-writer.ts -import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { join as join2 } from "path"; -import { homedir as homedir2 } from "os"; -function expandDataDir(dataDir) { - if (dataDir.startsWith("~")) { - return join2(homedir2(), dataDir.slice(1)); - } - return dataDir; -} -function buildSettingsObject(providerConfig, settingsConfig) { - const settings = { - CLAUDE_MEM_WORKER_PORT: settingsConfig.workerPort, - CLAUDE_MEM_WORKER_HOST: "127.0.0.1", - CLAUDE_MEM_DATA_DIR: expandDataDir(settingsConfig.dataDir), - CLAUDE_MEM_CONTEXT_OBSERVATIONS: settingsConfig.contextObservations, - CLAUDE_MEM_LOG_LEVEL: settingsConfig.logLevel, - CLAUDE_MEM_PYTHON_VERSION: settingsConfig.pythonVersion, - CLAUDE_MEM_PROVIDER: providerConfig.provider - }; - if (providerConfig.provider === "claude") { - settings.CLAUDE_MEM_CLAUDE_AUTH_METHOD = providerConfig.claudeAuthMethod ?? "cli"; - } - if (providerConfig.provider === "gemini") { - if (providerConfig.apiKey) settings.CLAUDE_MEM_GEMINI_API_KEY = providerConfig.apiKey; - if (providerConfig.model) settings.CLAUDE_MEM_GEMINI_MODEL = providerConfig.model; - settings.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED = providerConfig.rateLimitingEnabled !== false ? "true" : "false"; - } - if (providerConfig.provider === "openrouter") { - if (providerConfig.apiKey) settings.CLAUDE_MEM_OPENROUTER_API_KEY = providerConfig.apiKey; - if (providerConfig.model) settings.CLAUDE_MEM_OPENROUTER_MODEL = providerConfig.model; - } - if (settingsConfig.chromaEnabled) { - settings.CLAUDE_MEM_CHROMA_MODE = settingsConfig.chromaMode ?? "local"; - if (settingsConfig.chromaMode === "remote") { - if (settingsConfig.chromaHost) settings.CLAUDE_MEM_CHROMA_HOST = settingsConfig.chromaHost; - if (settingsConfig.chromaPort) settings.CLAUDE_MEM_CHROMA_PORT = settingsConfig.chromaPort; - if (settingsConfig.chromaSsl !== void 0) settings.CLAUDE_MEM_CHROMA_SSL = String(settingsConfig.chromaSsl); - } - } - return settings; -} -function writeSettings(providerConfig, settingsConfig) { - const dataDir = expandDataDir(settingsConfig.dataDir); - const settingsPath = join2(dataDir, "settings.json"); - if (!existsSync3(dataDir)) { - mkdirSync(dataDir, { recursive: true }); - } - let existingSettings = {}; - if (existsSync3(settingsPath)) { - const raw = readFileSync(settingsPath, "utf-8"); - existingSettings = JSON.parse(raw); - } - const newSettings = buildSettingsObject(providerConfig, settingsConfig); - const merged = { ...existingSettings, ...newSettings }; - writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8"); -} - -// src/steps/install.ts -var import_picocolors6 = __toESM(require_picocolors(), 1); -import { execSync as execSync3 } from "child_process"; -import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, cpSync } from "fs"; -import { join as join3 } from "path"; -import { homedir as homedir3, tmpdir } from "os"; -var MARKETPLACE_DIR = join3(homedir3(), ".claude", "plugins", "marketplaces", "thedotmack"); -var PLUGINS_DIR = join3(homedir3(), ".claude", "plugins"); -var CLAUDE_SETTINGS_PATH = join3(homedir3(), ".claude", "settings.json"); -function ensureDir(directoryPath) { - if (!existsSync4(directoryPath)) { - mkdirSync2(directoryPath, { recursive: true }); - } -} -function readJsonFile(filepath) { - if (!existsSync4(filepath)) return {}; - return JSON.parse(readFileSync2(filepath, "utf-8")); -} -function writeJsonFile(filepath, data) { - ensureDir(join3(filepath, "..")); - writeFileSync2(filepath, JSON.stringify(data, null, 2) + "\n", "utf-8"); -} -function registerMarketplace() { - const knownMarketplacesPath = join3(PLUGINS_DIR, "known_marketplaces.json"); - const knownMarketplaces = readJsonFile(knownMarketplacesPath); - knownMarketplaces["thedotmack"] = { - source: { - source: "github", - repo: "thedotmack/claude-mem" - }, - installLocation: MARKETPLACE_DIR, - lastUpdated: (/* @__PURE__ */ new Date()).toISOString(), - autoUpdate: true - }; - ensureDir(PLUGINS_DIR); - writeJsonFile(knownMarketplacesPath, knownMarketplaces); -} -function registerPlugin(version) { - const installedPluginsPath = join3(PLUGINS_DIR, "installed_plugins.json"); - const installedPlugins = readJsonFile(installedPluginsPath); - if (!installedPlugins.version) installedPlugins.version = 2; - if (!installedPlugins.plugins) installedPlugins.plugins = {}; - const pluginCachePath = join3(PLUGINS_DIR, "cache", "thedotmack", "claude-mem", version); - const now = (/* @__PURE__ */ new Date()).toISOString(); - installedPlugins.plugins["claude-mem@thedotmack"] = [ - { - scope: "user", - installPath: pluginCachePath, - version, - installedAt: now, - lastUpdated: now - } - ]; - writeJsonFile(installedPluginsPath, installedPlugins); - ensureDir(pluginCachePath); - const pluginSourceDir = join3(MARKETPLACE_DIR, "plugin"); - if (existsSync4(pluginSourceDir)) { - cpSync(pluginSourceDir, pluginCachePath, { recursive: true }); - } -} -function enablePluginInClaudeSettings() { - const settings = readJsonFile(CLAUDE_SETTINGS_PATH); - if (!settings.enabledPlugins) settings.enabledPlugins = {}; - settings.enabledPlugins["claude-mem@thedotmack"] = true; - writeJsonFile(CLAUDE_SETTINGS_PATH, settings); -} -function getPluginVersion() { - const pluginJsonPath = join3(MARKETPLACE_DIR, "plugin", ".claude-plugin", "plugin.json"); - if (existsSync4(pluginJsonPath)) { - const pluginJson = JSON.parse(readFileSync2(pluginJsonPath, "utf-8")); - return pluginJson.version ?? "1.0.0"; - } - return "1.0.0"; -} -async function runInstallation(selectedIDEs) { - const tempDir = join3(tmpdir(), `claude-mem-install-${Date.now()}`); - await Ye([ - { - title: "Cloning claude-mem repository", - task: async (message) => { - message("Downloading latest release..."); - execSync3( - `git clone --depth 1 https://github.com/thedotmack/claude-mem.git "${tempDir}"`, - { stdio: "pipe" } - ); - return `Repository cloned ${import_picocolors6.default.green("OK")}`; - } - }, - { - title: "Installing dependencies", - task: async (message) => { - message("Running npm install..."); - execSync3("npm install", { cwd: tempDir, stdio: "pipe" }); - return `Dependencies installed ${import_picocolors6.default.green("OK")}`; - } - }, - { - title: "Building plugin", - task: async (message) => { - message("Compiling TypeScript and bundling..."); - execSync3("npm run build", { cwd: tempDir, stdio: "pipe" }); - return `Plugin built ${import_picocolors6.default.green("OK")}`; - } - }, - { - title: "Registering plugin", - task: async (message) => { - message("Copying files to marketplace directory..."); - ensureDir(MARKETPLACE_DIR); - execSync3( - `rsync -a --delete --exclude=.git --exclude=package-lock.json --exclude=bun.lock "${tempDir}/" "${MARKETPLACE_DIR}/"`, - { stdio: "pipe" } - ); - message("Registering marketplace..."); - registerMarketplace(); - message("Installing marketplace dependencies..."); - execSync3("npm install", { cwd: MARKETPLACE_DIR, stdio: "pipe" }); - message("Registering plugin in Claude Code..."); - const version = getPluginVersion(); - registerPlugin(version); - message("Enabling plugin..."); - enablePluginInClaudeSettings(); - return `Plugin registered (v${getPluginVersion()}) ${import_picocolors6.default.green("OK")}`; - } - } - ]); - try { - execSync3(`rm -rf "${tempDir}"`, { stdio: "pipe" }); - } catch { - } - if (selectedIDEs.includes("cursor")) { - R2.info("Cursor hook configuration will be available after first launch."); - R2.info("Run: claude-mem cursor-setup (coming soon)"); - } -} - -// src/steps/worker.ts -var import_picocolors7 = __toESM(require_picocolors(), 1); -import { spawn } from "child_process"; -import { join as join4 } from "path"; -import { homedir as homedir4 } from "os"; -var MARKETPLACE_DIR2 = join4(homedir4(), ".claude", "plugins", "marketplaces", "thedotmack"); -var HEALTH_CHECK_INTERVAL_MS = 1e3; -var HEALTH_CHECK_MAX_ATTEMPTS = 30; -async function pollHealthEndpoint(port, maxAttempts = HEALTH_CHECK_MAX_ATTEMPTS) { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - try { - const response = await fetch(`http://127.0.0.1:${port}/api/health`); - if (response.ok) return true; - } catch { - } - await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS)); - } - return false; -} -async function runWorkerStartup(workerPort, dataDir) { - const bunInfo = findBinary("bun", ["~/.bun/bin/bun", "/usr/local/bin/bun", "/opt/homebrew/bin/bun"]); - if (!bunInfo.found || !bunInfo.path) { - R2.error("Bun is required to start the worker but was not found."); - R2.info("Install Bun: curl -fsSL https://bun.sh/install | bash"); - return; - } - const workerScript = join4(MARKETPLACE_DIR2, "plugin", "scripts", "worker-service.cjs"); - const expandedDataDir = expandHome(dataDir); - const logPath = join4(expandedDataDir, "logs"); - const s = bt2(); - s.start("Starting worker service..."); - const child = spawn(bunInfo.path, [workerScript], { - cwd: MARKETPLACE_DIR2, - detached: true, - stdio: "ignore", - env: { - ...process.env, - CLAUDE_MEM_WORKER_PORT: workerPort, - CLAUDE_MEM_DATA_DIR: expandedDataDir - } - }); - child.unref(); - const workerIsHealthy = await pollHealthEndpoint(workerPort); - if (workerIsHealthy) { - s.stop(`Worker running on port ${import_picocolors7.default.cyan(workerPort)} ${import_picocolors7.default.green("OK")}`); - } else { - s.stop(`Worker may still be starting. Check logs at: ${logPath}`); - R2.warn("Health check timed out. The worker might need more time to initialize."); - R2.info(`Check status: curl http://127.0.0.1:${workerPort}/api/health`); - } -} - -// src/steps/complete.ts -var import_picocolors8 = __toESM(require_picocolors(), 1); -function getProviderLabel(config) { - switch (config.provider) { - case "claude": - return config.claudeAuthMethod === "api" ? "Claude (API Key)" : "Claude (CLI subscription)"; - case "gemini": - return `Gemini (${config.model ?? "gemini-2.5-flash-lite"})`; - case "openrouter": - return `OpenRouter (${config.model ?? "xiaomi/mimo-v2-flash:free"})`; - } -} -function getIDELabels(ides) { - return ides.map((ide) => { - switch (ide) { - case "claude-code": - return "Claude Code"; - case "cursor": - return "Cursor"; - } - }).join(", "); -} -function runCompletion(providerConfig, settingsConfig, selectedIDEs) { - const summaryLines = [ - `Provider: ${import_picocolors8.default.cyan(getProviderLabel(providerConfig))}`, - `IDEs: ${import_picocolors8.default.cyan(getIDELabels(selectedIDEs))}`, - `Data dir: ${import_picocolors8.default.cyan(settingsConfig.dataDir)}`, - `Port: ${import_picocolors8.default.cyan(settingsConfig.workerPort)}`, - `Chroma: ${settingsConfig.chromaEnabled ? import_picocolors8.default.green("enabled") : import_picocolors8.default.dim("disabled")}` - ]; - Ve(summaryLines.join("\n"), "Configuration Summary"); - const nextStepsLines = []; - if (selectedIDEs.includes("claude-code")) { - nextStepsLines.push("Open Claude Code and start a conversation \u2014 memory is automatic!"); - } - if (selectedIDEs.includes("cursor")) { - nextStepsLines.push("Open Cursor \u2014 hooks are active in your projects."); - } - nextStepsLines.push(`View your memories: ${import_picocolors8.default.underline(`http://localhost:${settingsConfig.workerPort}`)}`); - nextStepsLines.push(`Search past work: use ${import_picocolors8.default.bold("/mem-search")} in Claude Code`); - Ve(nextStepsLines.join("\n"), "Next Steps"); - Le(import_picocolors8.default.green("claude-mem installed successfully!")); -} - -// src/index.ts -async function runInstaller() { - if (!process.stdin.isTTY) { - console.error("Error: This installer requires an interactive terminal."); - console.error("Run directly: npx claude-mem-installer"); - process.exit(1); - } - const installMode = await runWelcome(); - await runDependencyChecks(); - const selectedIDEs = await runIdeSelection(); - const providerConfig = await runProviderConfiguration(); - const settingsConfig = await runSettingsConfiguration(); - writeSettings(providerConfig, settingsConfig); - R2.success("Settings saved."); - if (installMode !== "configure") { - await runInstallation(selectedIDEs); - await runWorkerStartup(settingsConfig.workerPort, settingsConfig.dataDir); - } - runCompletion(providerConfig, settingsConfig, selectedIDEs); -} -runInstaller().catch((error) => { - Ne("Installation failed."); - console.error(error); - process.exit(1); -}); +// claude-mem installer redirect +// The old bundled installer has been replaced by npx claude-mem. +// This script now redirects users to the new install method. + +console.log(''); +console.log('\x1b[33mThe bundled installer has been replaced.\x1b[0m'); +console.log(''); +console.log('\x1b[32mInstall claude-mem with:\x1b[0m'); +console.log(''); +console.log(' \x1b[36mnpx claude-mem install\x1b[0m'); +console.log(''); +console.log('For more info, visit: \x1b[36mhttps://docs.claude-mem.ai/installation\x1b[0m'); +console.log(''); + +process.exit(0); diff --git a/installer/build.mjs b/installer/build.mjs deleted file mode 100644 index 6af63d503..000000000 --- a/installer/build.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import { build } from 'esbuild'; - -await build({ - entryPoints: ['src/index.ts'], - bundle: true, - format: 'esm', - platform: 'node', - target: 'node18', - outfile: 'dist/index.js', - banner: { - js: '#!/usr/bin/env node', - }, - external: [], -}); - -console.log('Build complete: dist/index.js'); diff --git a/installer/dist/index.js b/installer/dist/index.js deleted file mode 100644 index 739292992..000000000 --- a/installer/dist/index.js +++ /dev/null @@ -1,2107 +0,0 @@ -#!/usr/bin/env node -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); - -// node_modules/picocolors/picocolors.js -var require_picocolors = __commonJS({ - "node_modules/picocolors/picocolors.js"(exports, module) { - var p = process || {}; - var argv = p.argv || []; - var env = p.env || {}; - var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI); - var formatter = (open, close, replace = open) => (input) => { - let string = "" + input, index = string.indexOf(close, open.length); - return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close; - }; - var replaceClose = (string, close, replace, index) => { - let result = "", cursor = 0; - do { - result += string.substring(cursor, index) + replace; - cursor = index + close.length; - index = string.indexOf(close, cursor); - } while (~index); - return result + string.substring(cursor); - }; - var createColors = (enabled = isColorSupported) => { - let f = enabled ? formatter : () => String; - return { - isColorSupported: enabled, - reset: f("\x1B[0m", "\x1B[0m"), - bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"), - dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"), - italic: f("\x1B[3m", "\x1B[23m"), - underline: f("\x1B[4m", "\x1B[24m"), - inverse: f("\x1B[7m", "\x1B[27m"), - hidden: f("\x1B[8m", "\x1B[28m"), - strikethrough: f("\x1B[9m", "\x1B[29m"), - black: f("\x1B[30m", "\x1B[39m"), - red: f("\x1B[31m", "\x1B[39m"), - green: f("\x1B[32m", "\x1B[39m"), - yellow: f("\x1B[33m", "\x1B[39m"), - blue: f("\x1B[34m", "\x1B[39m"), - magenta: f("\x1B[35m", "\x1B[39m"), - cyan: f("\x1B[36m", "\x1B[39m"), - white: f("\x1B[37m", "\x1B[39m"), - gray: f("\x1B[90m", "\x1B[39m"), - bgBlack: f("\x1B[40m", "\x1B[49m"), - bgRed: f("\x1B[41m", "\x1B[49m"), - bgGreen: f("\x1B[42m", "\x1B[49m"), - bgYellow: f("\x1B[43m", "\x1B[49m"), - bgBlue: f("\x1B[44m", "\x1B[49m"), - bgMagenta: f("\x1B[45m", "\x1B[49m"), - bgCyan: f("\x1B[46m", "\x1B[49m"), - bgWhite: f("\x1B[47m", "\x1B[49m"), - blackBright: f("\x1B[90m", "\x1B[39m"), - redBright: f("\x1B[91m", "\x1B[39m"), - greenBright: f("\x1B[92m", "\x1B[39m"), - yellowBright: f("\x1B[93m", "\x1B[39m"), - blueBright: f("\x1B[94m", "\x1B[39m"), - magentaBright: f("\x1B[95m", "\x1B[39m"), - cyanBright: f("\x1B[96m", "\x1B[39m"), - whiteBright: f("\x1B[97m", "\x1B[39m"), - bgBlackBright: f("\x1B[100m", "\x1B[49m"), - bgRedBright: f("\x1B[101m", "\x1B[49m"), - bgGreenBright: f("\x1B[102m", "\x1B[49m"), - bgYellowBright: f("\x1B[103m", "\x1B[49m"), - bgBlueBright: f("\x1B[104m", "\x1B[49m"), - bgMagentaBright: f("\x1B[105m", "\x1B[49m"), - bgCyanBright: f("\x1B[106m", "\x1B[49m"), - bgWhiteBright: f("\x1B[107m", "\x1B[49m") - }; - }; - module.exports = createColors(); - module.exports.createColors = createColors; - } -}); - -// node_modules/sisteransi/src/index.js -var require_src = __commonJS({ - "node_modules/sisteransi/src/index.js"(exports, module) { - "use strict"; - var ESC = "\x1B"; - var CSI = `${ESC}[`; - var beep = "\x07"; - var cursor = { - to(x3, y2) { - if (!y2) return `${CSI}${x3 + 1}G`; - return `${CSI}${y2 + 1};${x3 + 1}H`; - }, - move(x3, y2) { - let ret = ""; - if (x3 < 0) ret += `${CSI}${-x3}D`; - else if (x3 > 0) ret += `${CSI}${x3}C`; - if (y2 < 0) ret += `${CSI}${-y2}A`; - else if (y2 > 0) ret += `${CSI}${y2}B`; - return ret; - }, - up: (count = 1) => `${CSI}${count}A`, - down: (count = 1) => `${CSI}${count}B`, - forward: (count = 1) => `${CSI}${count}C`, - backward: (count = 1) => `${CSI}${count}D`, - nextLine: (count = 1) => `${CSI}E`.repeat(count), - prevLine: (count = 1) => `${CSI}F`.repeat(count), - left: `${CSI}G`, - hide: `${CSI}?25l`, - show: `${CSI}?25h`, - save: `${ESC}7`, - restore: `${ESC}8` - }; - var scroll = { - up: (count = 1) => `${CSI}S`.repeat(count), - down: (count = 1) => `${CSI}T`.repeat(count) - }; - var erase = { - screen: `${CSI}2J`, - up: (count = 1) => `${CSI}1J`.repeat(count), - down: (count = 1) => `${CSI}J`.repeat(count), - line: `${CSI}2K`, - lineEnd: `${CSI}K`, - lineStart: `${CSI}1K`, - lines(count) { - let clear = ""; - for (let i = 0; i < count; i++) - clear += this.line + (i < count - 1 ? cursor.up() : ""); - if (count) - clear += cursor.left; - return clear; - } - }; - module.exports = { cursor, scroll, erase, beep }; - } -}); - -// node_modules/@clack/core/dist/index.mjs -var import_picocolors = __toESM(require_picocolors(), 1); -var import_sisteransi = __toESM(require_src(), 1); -import { stdout as R, stdin as q } from "node:process"; -import * as k from "node:readline"; -import ot from "node:readline"; -import { ReadStream as J } from "node:tty"; -function B(t, e2, s) { - if (!s.some((u) => !u.disabled)) return t; - const i = t + e2, r = Math.max(s.length - 1, 0), n = i < 0 ? r : i > r ? 0 : i; - return s[n].disabled ? B(n, e2 < 0 ? -1 : 1, s) : n; -} -var at = (t) => t === 161 || t === 164 || t === 167 || t === 168 || t === 170 || t === 173 || t === 174 || t >= 176 && t <= 180 || t >= 182 && t <= 186 || t >= 188 && t <= 191 || t === 198 || t === 208 || t === 215 || t === 216 || t >= 222 && t <= 225 || t === 230 || t >= 232 && t <= 234 || t === 236 || t === 237 || t === 240 || t === 242 || t === 243 || t >= 247 && t <= 250 || t === 252 || t === 254 || t === 257 || t === 273 || t === 275 || t === 283 || t === 294 || t === 295 || t === 299 || t >= 305 && t <= 307 || t === 312 || t >= 319 && t <= 322 || t === 324 || t >= 328 && t <= 331 || t === 333 || t === 338 || t === 339 || t === 358 || t === 359 || t === 363 || t === 462 || t === 464 || t === 466 || t === 468 || t === 470 || t === 472 || t === 474 || t === 476 || t === 593 || t === 609 || t === 708 || t === 711 || t >= 713 && t <= 715 || t === 717 || t === 720 || t >= 728 && t <= 731 || t === 733 || t === 735 || t >= 768 && t <= 879 || t >= 913 && t <= 929 || t >= 931 && t <= 937 || t >= 945 && t <= 961 || t >= 963 && t <= 969 || t === 1025 || t >= 1040 && t <= 1103 || t === 1105 || t === 8208 || t >= 8211 && t <= 8214 || t === 8216 || t === 8217 || t === 8220 || t === 8221 || t >= 8224 && t <= 8226 || t >= 8228 && t <= 8231 || t === 8240 || t === 8242 || t === 8243 || t === 8245 || t === 8251 || t === 8254 || t === 8308 || t === 8319 || t >= 8321 && t <= 8324 || t === 8364 || t === 8451 || t === 8453 || t === 8457 || t === 8467 || t === 8470 || t === 8481 || t === 8482 || t === 8486 || t === 8491 || t === 8531 || t === 8532 || t >= 8539 && t <= 8542 || t >= 8544 && t <= 8555 || t >= 8560 && t <= 8569 || t === 8585 || t >= 8592 && t <= 8601 || t === 8632 || t === 8633 || t === 8658 || t === 8660 || t === 8679 || t === 8704 || t === 8706 || t === 8707 || t === 8711 || t === 8712 || t === 8715 || t === 8719 || t === 8721 || t === 8725 || t === 8730 || t >= 8733 && t <= 8736 || t === 8739 || t === 8741 || t >= 8743 && t <= 8748 || t === 8750 || t >= 8756 && t <= 8759 || t === 8764 || t === 8765 || t === 8776 || t === 8780 || t === 8786 || t === 8800 || t === 8801 || t >= 8804 && t <= 8807 || t === 8810 || t === 8811 || t === 8814 || t === 8815 || t === 8834 || t === 8835 || t === 8838 || t === 8839 || t === 8853 || t === 8857 || t === 8869 || t === 8895 || t === 8978 || t >= 9312 && t <= 9449 || t >= 9451 && t <= 9547 || t >= 9552 && t <= 9587 || t >= 9600 && t <= 9615 || t >= 9618 && t <= 9621 || t === 9632 || t === 9633 || t >= 9635 && t <= 9641 || t === 9650 || t === 9651 || t === 9654 || t === 9655 || t === 9660 || t === 9661 || t === 9664 || t === 9665 || t >= 9670 && t <= 9672 || t === 9675 || t >= 9678 && t <= 9681 || t >= 9698 && t <= 9701 || t === 9711 || t === 9733 || t === 9734 || t === 9737 || t === 9742 || t === 9743 || t === 9756 || t === 9758 || t === 9792 || t === 9794 || t === 9824 || t === 9825 || t >= 9827 && t <= 9829 || t >= 9831 && t <= 9834 || t === 9836 || t === 9837 || t === 9839 || t === 9886 || t === 9887 || t === 9919 || t >= 9926 && t <= 9933 || t >= 9935 && t <= 9939 || t >= 9941 && t <= 9953 || t === 9955 || t === 9960 || t === 9961 || t >= 9963 && t <= 9969 || t === 9972 || t >= 9974 && t <= 9977 || t === 9979 || t === 9980 || t === 9982 || t === 9983 || t === 10045 || t >= 10102 && t <= 10111 || t >= 11094 && t <= 11097 || t >= 12872 && t <= 12879 || t >= 57344 && t <= 63743 || t >= 65024 && t <= 65039 || t === 65533 || t >= 127232 && t <= 127242 || t >= 127248 && t <= 127277 || t >= 127280 && t <= 127337 || t >= 127344 && t <= 127373 || t === 127375 || t === 127376 || t >= 127387 && t <= 127404 || t >= 917760 && t <= 917999 || t >= 983040 && t <= 1048573 || t >= 1048576 && t <= 1114109; -var lt = (t) => t === 12288 || t >= 65281 && t <= 65376 || t >= 65504 && t <= 65510; -var ht = (t) => t >= 4352 && t <= 4447 || t === 8986 || t === 8987 || t === 9001 || t === 9002 || t >= 9193 && t <= 9196 || t === 9200 || t === 9203 || t === 9725 || t === 9726 || t === 9748 || t === 9749 || t >= 9800 && t <= 9811 || t === 9855 || t === 9875 || t === 9889 || t === 9898 || t === 9899 || t === 9917 || t === 9918 || t === 9924 || t === 9925 || t === 9934 || t === 9940 || t === 9962 || t === 9970 || t === 9971 || t === 9973 || t === 9978 || t === 9981 || t === 9989 || t === 9994 || t === 9995 || t === 10024 || t === 10060 || t === 10062 || t >= 10067 && t <= 10069 || t === 10071 || t >= 10133 && t <= 10135 || t === 10160 || t === 10175 || t === 11035 || t === 11036 || t === 11088 || t === 11093 || t >= 11904 && t <= 11929 || t >= 11931 && t <= 12019 || t >= 12032 && t <= 12245 || t >= 12272 && t <= 12287 || t >= 12289 && t <= 12350 || t >= 12353 && t <= 12438 || t >= 12441 && t <= 12543 || t >= 12549 && t <= 12591 || t >= 12593 && t <= 12686 || t >= 12688 && t <= 12771 || t >= 12783 && t <= 12830 || t >= 12832 && t <= 12871 || t >= 12880 && t <= 19903 || t >= 19968 && t <= 42124 || t >= 42128 && t <= 42182 || t >= 43360 && t <= 43388 || t >= 44032 && t <= 55203 || t >= 63744 && t <= 64255 || t >= 65040 && t <= 65049 || t >= 65072 && t <= 65106 || t >= 65108 && t <= 65126 || t >= 65128 && t <= 65131 || t >= 94176 && t <= 94180 || t === 94192 || t === 94193 || t >= 94208 && t <= 100343 || t >= 100352 && t <= 101589 || t >= 101632 && t <= 101640 || t >= 110576 && t <= 110579 || t >= 110581 && t <= 110587 || t === 110589 || t === 110590 || t >= 110592 && t <= 110882 || t === 110898 || t >= 110928 && t <= 110930 || t === 110933 || t >= 110948 && t <= 110951 || t >= 110960 && t <= 111355 || t === 126980 || t === 127183 || t === 127374 || t >= 127377 && t <= 127386 || t >= 127488 && t <= 127490 || t >= 127504 && t <= 127547 || t >= 127552 && t <= 127560 || t === 127568 || t === 127569 || t >= 127584 && t <= 127589 || t >= 127744 && t <= 127776 || t >= 127789 && t <= 127797 || t >= 127799 && t <= 127868 || t >= 127870 && t <= 127891 || t >= 127904 && t <= 127946 || t >= 127951 && t <= 127955 || t >= 127968 && t <= 127984 || t === 127988 || t >= 127992 && t <= 128062 || t === 128064 || t >= 128066 && t <= 128252 || t >= 128255 && t <= 128317 || t >= 128331 && t <= 128334 || t >= 128336 && t <= 128359 || t === 128378 || t === 128405 || t === 128406 || t === 128420 || t >= 128507 && t <= 128591 || t >= 128640 && t <= 128709 || t === 128716 || t >= 128720 && t <= 128722 || t >= 128725 && t <= 128727 || t >= 128732 && t <= 128735 || t === 128747 || t === 128748 || t >= 128756 && t <= 128764 || t >= 128992 && t <= 129003 || t === 129008 || t >= 129292 && t <= 129338 || t >= 129340 && t <= 129349 || t >= 129351 && t <= 129535 || t >= 129648 && t <= 129660 || t >= 129664 && t <= 129672 || t >= 129680 && t <= 129725 || t >= 129727 && t <= 129733 || t >= 129742 && t <= 129755 || t >= 129760 && t <= 129768 || t >= 129776 && t <= 129784 || t >= 131072 && t <= 196605 || t >= 196608 && t <= 262141; -var O = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/y; -var y = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y; -var L = /\t{1,1000}/y; -var P = new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*", "yu"); -var M = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y; -var ct = new RegExp("\\p{M}+", "gu"); -var ft = { limit: 1 / 0, ellipsis: "" }; -var X = (t, e2 = {}, s = {}) => { - const i = e2.limit ?? 1 / 0, r = e2.ellipsis ?? "", n = e2?.ellipsisWidth ?? (r ? X(r, ft, s).width : 0), u = s.ansiWidth ?? 0, a = s.controlWidth ?? 0, l = s.tabWidth ?? 8, E = s.ambiguousWidth ?? 1, g = s.emojiWidth ?? 2, m = s.fullWidthWidth ?? 2, A = s.regularWidth ?? 1, V2 = s.wideWidth ?? 2; - let h = 0, o = 0, p = t.length, v = 0, F = false, d2 = p, b = Math.max(0, i - n), C2 = 0, w = 0, c = 0, f = 0; - t: for (; ; ) { - if (w > C2 || o >= p && o > h) { - const ut = t.slice(C2, w) || t.slice(h, o); - v = 0; - for (const Y of ut.replaceAll(ct, "")) { - const $ = Y.codePointAt(0) || 0; - if (lt($) ? f = m : ht($) ? f = V2 : E !== A && at($) ? f = E : f = A, c + f > b && (d2 = Math.min(d2, Math.max(C2, h) + v)), c + f > i) { - F = true; - break t; - } - v += Y.length, c += f; - } - C2 = w = 0; - } - if (o >= p) break; - if (M.lastIndex = o, M.test(t)) { - if (v = M.lastIndex - o, f = v * A, c + f > b && (d2 = Math.min(d2, o + Math.floor((b - c) / A))), c + f > i) { - F = true; - break; - } - c += f, C2 = h, w = o, o = h = M.lastIndex; - continue; - } - if (O.lastIndex = o, O.test(t)) { - if (c + u > b && (d2 = Math.min(d2, o)), c + u > i) { - F = true; - break; - } - c += u, C2 = h, w = o, o = h = O.lastIndex; - continue; - } - if (y.lastIndex = o, y.test(t)) { - if (v = y.lastIndex - o, f = v * a, c + f > b && (d2 = Math.min(d2, o + Math.floor((b - c) / a))), c + f > i) { - F = true; - break; - } - c += f, C2 = h, w = o, o = h = y.lastIndex; - continue; - } - if (L.lastIndex = o, L.test(t)) { - if (v = L.lastIndex - o, f = v * l, c + f > b && (d2 = Math.min(d2, o + Math.floor((b - c) / l))), c + f > i) { - F = true; - break; - } - c += f, C2 = h, w = o, o = h = L.lastIndex; - continue; - } - if (P.lastIndex = o, P.test(t)) { - if (c + g > b && (d2 = Math.min(d2, o)), c + g > i) { - F = true; - break; - } - c += g, C2 = h, w = o, o = h = P.lastIndex; - continue; - } - o += 1; - } - return { width: F ? b : c, index: F ? d2 : p, truncated: F, ellipsed: F && i >= n }; -}; -var pt = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; -var S = (t, e2 = {}) => X(t, pt, e2).width; -var W = "\x1B"; -var Z = "\x9B"; -var Ft = 39; -var j = "\x07"; -var Q = "["; -var dt = "]"; -var tt = "m"; -var U = `${dt}8;;`; -var et = new RegExp(`(?:\\${Q}(?\\d+)m|\\${U}(?.*)${j})`, "y"); -var mt = (t) => { - if (t >= 30 && t <= 37 || t >= 90 && t <= 97) return 39; - if (t >= 40 && t <= 47 || t >= 100 && t <= 107) return 49; - if (t === 1 || t === 2) return 22; - if (t === 3) return 23; - if (t === 4) return 24; - if (t === 7) return 27; - if (t === 8) return 28; - if (t === 9) return 29; - if (t === 0) return 0; -}; -var st = (t) => `${W}${Q}${t}${tt}`; -var it = (t) => `${W}${U}${t}${j}`; -var gt = (t) => t.map((e2) => S(e2)); -var G = (t, e2, s) => { - const i = e2[Symbol.iterator](); - let r = false, n = false, u = t.at(-1), a = u === void 0 ? 0 : S(u), l = i.next(), E = i.next(), g = 0; - for (; !l.done; ) { - const m = l.value, A = S(m); - a + A <= s ? t[t.length - 1] += m : (t.push(m), a = 0), (m === W || m === Z) && (r = true, n = e2.startsWith(U, g + 1)), r ? n ? m === j && (r = false, n = false) : m === tt && (r = false) : (a += A, a === s && !E.done && (t.push(""), a = 0)), l = E, E = i.next(), g += m.length; - } - u = t.at(-1), !a && u !== void 0 && u.length > 0 && t.length > 1 && (t[t.length - 2] += t.pop()); -}; -var vt = (t) => { - const e2 = t.split(" "); - let s = e2.length; - for (; s > 0 && !(S(e2[s - 1]) > 0); ) s--; - return s === e2.length ? t : e2.slice(0, s).join(" ") + e2.slice(s).join(""); -}; -var Et = (t, e2, s = {}) => { - if (s.trim !== false && t.trim() === "") return ""; - let i = "", r, n; - const u = t.split(" "), a = gt(u); - let l = [""]; - for (const [h, o] of u.entries()) { - s.trim !== false && (l[l.length - 1] = (l.at(-1) ?? "").trimStart()); - let p = S(l.at(-1) ?? ""); - if (h !== 0 && (p >= e2 && (s.wordWrap === false || s.trim === false) && (l.push(""), p = 0), (p > 0 || s.trim === false) && (l[l.length - 1] += " ", p++)), s.hard && a[h] > e2) { - const v = e2 - p, F = 1 + Math.floor((a[h] - v - 1) / e2); - Math.floor((a[h] - 1) / e2) < F && l.push(""), G(l, o, e2); - continue; - } - if (p + a[h] > e2 && p > 0 && a[h] > 0) { - if (s.wordWrap === false && p < e2) { - G(l, o, e2); - continue; - } - l.push(""); - } - if (p + a[h] > e2 && s.wordWrap === false) { - G(l, o, e2); - continue; - } - l[l.length - 1] += o; - } - s.trim !== false && (l = l.map((h) => vt(h))); - const E = l.join(` -`), g = E[Symbol.iterator](); - let m = g.next(), A = g.next(), V2 = 0; - for (; !m.done; ) { - const h = m.value, o = A.value; - if (i += h, h === W || h === Z) { - et.lastIndex = V2 + 1; - const F = et.exec(E)?.groups; - if (F?.code !== void 0) { - const d2 = Number.parseFloat(F.code); - r = d2 === Ft ? void 0 : d2; - } else F?.uri !== void 0 && (n = F.uri.length === 0 ? void 0 : F.uri); - } - const p = r ? mt(r) : void 0; - o === ` -` ? (n && (i += it("")), r && p && (i += st(p))) : h === ` -` && (r && p && (i += st(r)), n && (i += it(n))), V2 += h.length, m = A, A = g.next(); - } - return i; -}; -function K(t, e2, s) { - return String(t).normalize().replaceAll(`\r -`, ` -`).split(` -`).map((i) => Et(i, e2, s)).join(` -`); -} -var At = ["up", "down", "left", "right", "space", "enter", "cancel"]; -var _ = { actions: new Set(At), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]), messages: { cancel: "Canceled", error: "Something went wrong" }, withGuide: true }; -function H(t, e2) { - if (typeof t == "string") return _.aliases.get(t) === e2; - for (const s of t) if (s !== void 0 && H(s, e2)) return true; - return false; -} -function _t(t, e2) { - if (t === e2) return; - const s = t.split(` -`), i = e2.split(` -`), r = Math.max(s.length, i.length), n = []; - for (let u = 0; u < r; u++) s[u] !== i[u] && n.push(u); - return { lines: n, numLinesBefore: s.length, numLinesAfter: i.length, numLines: r }; -} -var bt = globalThis.process.platform.startsWith("win"); -var z = Symbol("clack:cancel"); -function Ct(t) { - return t === z; -} -function T(t, e2) { - const s = t; - s.isTTY && s.setRawMode(e2); -} -function Bt({ input: t = q, output: e2 = R, overwrite: s = true, hideCursor: i = true } = {}) { - const r = k.createInterface({ input: t, output: e2, prompt: "", tabSize: 1 }); - k.emitKeypressEvents(t, r), t instanceof J && t.isTTY && t.setRawMode(true); - const n = (u, { name: a, sequence: l }) => { - const E = String(u); - if (H([E, a, l], "cancel")) { - i && e2.write(import_sisteransi.cursor.show), process.exit(0); - return; - } - if (!s) return; - const g = a === "return" ? 0 : -1, m = a === "return" ? -1 : 0; - k.moveCursor(e2, g, m, () => { - k.clearLine(e2, 1, () => { - t.once("keypress", n); - }); - }); - }; - return i && e2.write(import_sisteransi.cursor.hide), t.once("keypress", n), () => { - t.off("keypress", n), i && e2.write(import_sisteransi.cursor.show), t instanceof J && t.isTTY && !bt && t.setRawMode(false), r.terminal = false, r.close(); - }; -} -var rt = (t) => "columns" in t && typeof t.columns == "number" ? t.columns : 80; -var nt = (t) => "rows" in t && typeof t.rows == "number" ? t.rows : 20; -function xt(t, e2, s, i = s) { - const r = rt(t ?? R); - return K(e2, r - s.length, { hard: true, trim: false }).split(` -`).map((n, u) => `${u === 0 ? i : s}${n}`).join(` -`); -} -var x = class { - input; - output; - _abortSignal; - rl; - opts; - _render; - _track = false; - _prevFrame = ""; - _subscribers = /* @__PURE__ */ new Map(); - _cursor = 0; - state = "initial"; - error = ""; - value; - userInput = ""; - constructor(e2, s = true) { - const { input: i = q, output: r = R, render: n, signal: u, ...a } = e2; - this.opts = a, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = n.bind(this), this._track = s, this._abortSignal = u, this.input = i, this.output = r; - } - unsubscribe() { - this._subscribers.clear(); - } - setSubscriber(e2, s) { - const i = this._subscribers.get(e2) ?? []; - i.push(s), this._subscribers.set(e2, i); - } - on(e2, s) { - this.setSubscriber(e2, { cb: s }); - } - once(e2, s) { - this.setSubscriber(e2, { cb: s, once: true }); - } - emit(e2, ...s) { - const i = this._subscribers.get(e2) ?? [], r = []; - for (const n of i) n.cb(...s), n.once && r.push(() => i.splice(i.indexOf(n), 1)); - for (const n of r) n(); - } - prompt() { - return new Promise((e2) => { - if (this._abortSignal) { - if (this._abortSignal.aborted) return this.state = "cancel", this.close(), e2(z); - this._abortSignal.addEventListener("abort", () => { - this.state = "cancel", this.close(); - }, { once: true }); - } - this.rl = ot.createInterface({ input: this.input, tabSize: 2, prompt: "", escapeCodeTimeout: 50, terminal: true }), this.rl.prompt(), this.opts.initialUserInput !== void 0 && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), T(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => { - this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), T(this.input, false), e2(this.value); - }), this.once("cancel", () => { - this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), T(this.input, false), e2(z); - }); - }); - } - _isActionKey(e2, s) { - return e2 === " "; - } - _setValue(e2) { - this.value = e2, this.emit("value", this.value); - } - _setUserInput(e2, s) { - this.userInput = e2 ?? "", this.emit("userInput", this.userInput), s && this._track && this.rl && (this.rl.write(this.userInput), this._cursor = this.rl.cursor); - } - _clearUserInput() { - this.rl?.write(null, { ctrl: true, name: "u" }), this._setUserInput(""); - } - onKeypress(e2, s) { - if (this._track && s.name !== "return" && (s.name && this._isActionKey(e2, s) && this.rl?.write(null, { ctrl: true, name: "h" }), this._cursor = this.rl?.cursor ?? 0, this._setUserInput(this.rl?.line)), this.state === "error" && (this.state = "active"), s?.name && (!this._track && _.aliases.has(s.name) && this.emit("cursor", _.aliases.get(s.name)), _.actions.has(s.name) && this.emit("cursor", s.name)), e2 && (e2.toLowerCase() === "y" || e2.toLowerCase() === "n") && this.emit("confirm", e2.toLowerCase() === "y"), this.emit("key", e2?.toLowerCase(), s), s?.name === "return") { - if (this.opts.validate) { - const i = this.opts.validate(this.value); - i && (this.error = i instanceof Error ? i.message : i, this.state = "error", this.rl?.write(this.userInput)); - } - this.state !== "error" && (this.state = "submit"); - } - H([e2, s?.name, s?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close(); - } - close() { - this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(` -`), T(this.input, false), this.rl?.close(), this.rl = void 0, this.emit(`${this.state}`, this.value), this.unsubscribe(); - } - restoreCursor() { - const e2 = K(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split(` -`).length - 1; - this.output.write(import_sisteransi.cursor.move(-999, e2 * -1)); - } - render() { - const e2 = K(this._render(this) ?? "", process.stdout.columns, { hard: true, trim: false }); - if (e2 !== this._prevFrame) { - if (this.state === "initial") this.output.write(import_sisteransi.cursor.hide); - else { - const s = _t(this._prevFrame, e2), i = nt(this.output); - if (this.restoreCursor(), s) { - const r = Math.max(0, s.numLinesAfter - i), n = Math.max(0, s.numLinesBefore - i); - let u = s.lines.find((a) => a >= r); - if (u === void 0) { - this._prevFrame = e2; - return; - } - if (s.lines.length === 1) { - this.output.write(import_sisteransi.cursor.move(0, u - n)), this.output.write(import_sisteransi.erase.lines(1)); - const a = e2.split(` -`); - this.output.write(a[u]), this._prevFrame = e2, this.output.write(import_sisteransi.cursor.move(0, a.length - u - 1)); - return; - } else if (s.lines.length > 1) { - if (r < n) u = r; - else { - const l = u - n; - l > 0 && this.output.write(import_sisteransi.cursor.move(0, l)); - } - this.output.write(import_sisteransi.erase.down()); - const a = e2.split(` -`).slice(u); - this.output.write(a.join(` -`)), this._prevFrame = e2; - return; - } - } - this.output.write(import_sisteransi.erase.down()); - } - this.output.write(e2), this.state === "initial" && (this.state = "active"), this._prevFrame = e2; - } - } -}; -var kt = class extends x { - get cursor() { - return this.value ? 0 : 1; - } - get _value() { - return this.cursor === 0; - } - constructor(e2) { - super(e2, false), this.value = !!e2.initialValue, this.on("userInput", () => { - this.value = this._value; - }), this.on("confirm", (s) => { - this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = s, this.state = "submit", this.close(); - }), this.on("cursor", () => { - this.value = !this.value; - }); - } -}; -var Lt = class extends x { - options; - cursor = 0; - get _value() { - return this.options[this.cursor].value; - } - get _enabledOptions() { - return this.options.filter((e2) => e2.disabled !== true); - } - toggleAll() { - const e2 = this._enabledOptions, s = this.value !== void 0 && this.value.length === e2.length; - this.value = s ? [] : e2.map((i) => i.value); - } - toggleInvert() { - const e2 = this.value; - if (!e2) return; - const s = this._enabledOptions.filter((i) => !e2.includes(i.value)); - this.value = s.map((i) => i.value); - } - toggleValue() { - this.value === void 0 && (this.value = []); - const e2 = this.value.includes(this._value); - this.value = e2 ? this.value.filter((s) => s !== this._value) : [...this.value, this._value]; - } - constructor(e2) { - super(e2, false), this.options = e2.options, this.value = [...e2.initialValues ?? []]; - const s = Math.max(this.options.findIndex(({ value: i }) => i === e2.cursorAt), 0); - this.cursor = this.options[s].disabled ? B(s, 1, this.options) : s, this.on("key", (i) => { - i === "a" && this.toggleAll(), i === "i" && this.toggleInvert(); - }), this.on("cursor", (i) => { - switch (i) { - case "left": - case "up": - this.cursor = B(this.cursor, -1, this.options); - break; - case "down": - case "right": - this.cursor = B(this.cursor, 1, this.options); - break; - case "space": - this.toggleValue(); - break; - } - }); - } -}; -var Mt = class extends x { - _mask = "\u2022"; - get cursor() { - return this._cursor; - } - get masked() { - return this.userInput.replaceAll(/./g, this._mask); - } - get userInputWithCursor() { - if (this.state === "submit" || this.state === "cancel") return this.masked; - const e2 = this.userInput; - if (this.cursor >= e2.length) return `${this.masked}${import_picocolors.default.inverse(import_picocolors.default.hidden("_"))}`; - const s = this.masked, i = s.slice(0, this.cursor), r = s.slice(this.cursor); - return `${i}${import_picocolors.default.inverse(r[0])}${r.slice(1)}`; - } - clear() { - this._clearUserInput(); - } - constructor({ mask: e2, ...s }) { - super(s), this._mask = e2 ?? "\u2022", this.on("userInput", (i) => { - this._setValue(i); - }); - } -}; -var Wt = class extends x { - options; - cursor = 0; - get _selectedValue() { - return this.options[this.cursor]; - } - changeValue() { - this.value = this._selectedValue.value; - } - constructor(e2) { - super(e2, false), this.options = e2.options; - const s = this.options.findIndex(({ value: r }) => r === e2.initialValue), i = s === -1 ? 0 : s; - this.cursor = this.options[i].disabled ? B(i, 1, this.options) : i, this.changeValue(), this.on("cursor", (r) => { - switch (r) { - case "left": - case "up": - this.cursor = B(this.cursor, -1, this.options); - break; - case "down": - case "right": - this.cursor = B(this.cursor, 1, this.options); - break; - } - this.changeValue(); - }); - } -}; -var $t = class extends x { - get userInputWithCursor() { - if (this.state === "submit") return this.userInput; - const e2 = this.userInput; - if (this.cursor >= e2.length) return `${this.userInput}\u2588`; - const s = e2.slice(0, this.cursor), [i, ...r] = e2.slice(this.cursor); - return `${s}${import_picocolors.default.inverse(i)}${r.join("")}`; - } - get cursor() { - return this._cursor; - } - constructor(e2) { - super({ ...e2, initialUserInput: e2.initialUserInput ?? e2.initialValue }), this.on("userInput", (s) => { - this._setValue(s); - }), this.on("finalize", () => { - this.value || (this.value = e2.defaultValue), this.value === void 0 && (this.value = ""); - }); - } -}; - -// node_modules/@clack/prompts/dist/index.mjs -var import_picocolors2 = __toESM(require_picocolors(), 1); -import N2 from "node:process"; -var import_sisteransi2 = __toESM(require_src(), 1); -function me() { - return N2.platform !== "win32" ? N2.env.TERM !== "linux" : !!N2.env.CI || !!N2.env.WT_SESSION || !!N2.env.TERMINUS_SUBLIME || N2.env.ConEmuTask === "{cmd::Cmder}" || N2.env.TERM_PROGRAM === "Terminus-Sublime" || N2.env.TERM_PROGRAM === "vscode" || N2.env.TERM === "xterm-256color" || N2.env.TERM === "alacritty" || N2.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; -} -var et2 = me(); -var ct2 = () => process.env.CI === "true"; -var C = (t, r) => et2 ? t : r; -var Rt = C("\u25C6", "*"); -var dt2 = C("\u25A0", "x"); -var $t2 = C("\u25B2", "x"); -var V = C("\u25C7", "o"); -var ht2 = C("\u250C", "T"); -var d = C("\u2502", "|"); -var x2 = C("\u2514", "\u2014"); -var Ot = C("\u2510", "T"); -var Pt = C("\u2518", "\u2014"); -var Q2 = C("\u25CF", ">"); -var H2 = C("\u25CB", " "); -var st2 = C("\u25FB", "[\u2022]"); -var U2 = C("\u25FC", "[+]"); -var q2 = C("\u25FB", "[ ]"); -var Nt = C("\u25AA", "\u2022"); -var rt2 = C("\u2500", "-"); -var mt2 = C("\u256E", "+"); -var Wt2 = C("\u251C", "+"); -var pt2 = C("\u256F", "+"); -var gt2 = C("\u2570", "+"); -var Lt2 = C("\u256D", "+"); -var ft2 = C("\u25CF", "\u2022"); -var Ft2 = C("\u25C6", "*"); -var yt2 = C("\u25B2", "!"); -var Et2 = C("\u25A0", "x"); -var W2 = (t) => { - switch (t) { - case "initial": - case "active": - return import_picocolors2.default.cyan(Rt); - case "cancel": - return import_picocolors2.default.red(dt2); - case "error": - return import_picocolors2.default.yellow($t2); - case "submit": - return import_picocolors2.default.green(V); - } -}; -var vt2 = (t) => { - switch (t) { - case "initial": - case "active": - return import_picocolors2.default.cyan(d); - case "cancel": - return import_picocolors2.default.red(d); - case "error": - return import_picocolors2.default.yellow(d); - case "submit": - return import_picocolors2.default.green(d); - } -}; -var pe = (t) => t === 161 || t === 164 || t === 167 || t === 168 || t === 170 || t === 173 || t === 174 || t >= 176 && t <= 180 || t >= 182 && t <= 186 || t >= 188 && t <= 191 || t === 198 || t === 208 || t === 215 || t === 216 || t >= 222 && t <= 225 || t === 230 || t >= 232 && t <= 234 || t === 236 || t === 237 || t === 240 || t === 242 || t === 243 || t >= 247 && t <= 250 || t === 252 || t === 254 || t === 257 || t === 273 || t === 275 || t === 283 || t === 294 || t === 295 || t === 299 || t >= 305 && t <= 307 || t === 312 || t >= 319 && t <= 322 || t === 324 || t >= 328 && t <= 331 || t === 333 || t === 338 || t === 339 || t === 358 || t === 359 || t === 363 || t === 462 || t === 464 || t === 466 || t === 468 || t === 470 || t === 472 || t === 474 || t === 476 || t === 593 || t === 609 || t === 708 || t === 711 || t >= 713 && t <= 715 || t === 717 || t === 720 || t >= 728 && t <= 731 || t === 733 || t === 735 || t >= 768 && t <= 879 || t >= 913 && t <= 929 || t >= 931 && t <= 937 || t >= 945 && t <= 961 || t >= 963 && t <= 969 || t === 1025 || t >= 1040 && t <= 1103 || t === 1105 || t === 8208 || t >= 8211 && t <= 8214 || t === 8216 || t === 8217 || t === 8220 || t === 8221 || t >= 8224 && t <= 8226 || t >= 8228 && t <= 8231 || t === 8240 || t === 8242 || t === 8243 || t === 8245 || t === 8251 || t === 8254 || t === 8308 || t === 8319 || t >= 8321 && t <= 8324 || t === 8364 || t === 8451 || t === 8453 || t === 8457 || t === 8467 || t === 8470 || t === 8481 || t === 8482 || t === 8486 || t === 8491 || t === 8531 || t === 8532 || t >= 8539 && t <= 8542 || t >= 8544 && t <= 8555 || t >= 8560 && t <= 8569 || t === 8585 || t >= 8592 && t <= 8601 || t === 8632 || t === 8633 || t === 8658 || t === 8660 || t === 8679 || t === 8704 || t === 8706 || t === 8707 || t === 8711 || t === 8712 || t === 8715 || t === 8719 || t === 8721 || t === 8725 || t === 8730 || t >= 8733 && t <= 8736 || t === 8739 || t === 8741 || t >= 8743 && t <= 8748 || t === 8750 || t >= 8756 && t <= 8759 || t === 8764 || t === 8765 || t === 8776 || t === 8780 || t === 8786 || t === 8800 || t === 8801 || t >= 8804 && t <= 8807 || t === 8810 || t === 8811 || t === 8814 || t === 8815 || t === 8834 || t === 8835 || t === 8838 || t === 8839 || t === 8853 || t === 8857 || t === 8869 || t === 8895 || t === 8978 || t >= 9312 && t <= 9449 || t >= 9451 && t <= 9547 || t >= 9552 && t <= 9587 || t >= 9600 && t <= 9615 || t >= 9618 && t <= 9621 || t === 9632 || t === 9633 || t >= 9635 && t <= 9641 || t === 9650 || t === 9651 || t === 9654 || t === 9655 || t === 9660 || t === 9661 || t === 9664 || t === 9665 || t >= 9670 && t <= 9672 || t === 9675 || t >= 9678 && t <= 9681 || t >= 9698 && t <= 9701 || t === 9711 || t === 9733 || t === 9734 || t === 9737 || t === 9742 || t === 9743 || t === 9756 || t === 9758 || t === 9792 || t === 9794 || t === 9824 || t === 9825 || t >= 9827 && t <= 9829 || t >= 9831 && t <= 9834 || t === 9836 || t === 9837 || t === 9839 || t === 9886 || t === 9887 || t === 9919 || t >= 9926 && t <= 9933 || t >= 9935 && t <= 9939 || t >= 9941 && t <= 9953 || t === 9955 || t === 9960 || t === 9961 || t >= 9963 && t <= 9969 || t === 9972 || t >= 9974 && t <= 9977 || t === 9979 || t === 9980 || t === 9982 || t === 9983 || t === 10045 || t >= 10102 && t <= 10111 || t >= 11094 && t <= 11097 || t >= 12872 && t <= 12879 || t >= 57344 && t <= 63743 || t >= 65024 && t <= 65039 || t === 65533 || t >= 127232 && t <= 127242 || t >= 127248 && t <= 127277 || t >= 127280 && t <= 127337 || t >= 127344 && t <= 127373 || t === 127375 || t === 127376 || t >= 127387 && t <= 127404 || t >= 917760 && t <= 917999 || t >= 983040 && t <= 1048573 || t >= 1048576 && t <= 1114109; -var ge = (t) => t === 12288 || t >= 65281 && t <= 65376 || t >= 65504 && t <= 65510; -var fe = (t) => t >= 4352 && t <= 4447 || t === 8986 || t === 8987 || t === 9001 || t === 9002 || t >= 9193 && t <= 9196 || t === 9200 || t === 9203 || t === 9725 || t === 9726 || t === 9748 || t === 9749 || t >= 9800 && t <= 9811 || t === 9855 || t === 9875 || t === 9889 || t === 9898 || t === 9899 || t === 9917 || t === 9918 || t === 9924 || t === 9925 || t === 9934 || t === 9940 || t === 9962 || t === 9970 || t === 9971 || t === 9973 || t === 9978 || t === 9981 || t === 9989 || t === 9994 || t === 9995 || t === 10024 || t === 10060 || t === 10062 || t >= 10067 && t <= 10069 || t === 10071 || t >= 10133 && t <= 10135 || t === 10160 || t === 10175 || t === 11035 || t === 11036 || t === 11088 || t === 11093 || t >= 11904 && t <= 11929 || t >= 11931 && t <= 12019 || t >= 12032 && t <= 12245 || t >= 12272 && t <= 12287 || t >= 12289 && t <= 12350 || t >= 12353 && t <= 12438 || t >= 12441 && t <= 12543 || t >= 12549 && t <= 12591 || t >= 12593 && t <= 12686 || t >= 12688 && t <= 12771 || t >= 12783 && t <= 12830 || t >= 12832 && t <= 12871 || t >= 12880 && t <= 19903 || t >= 19968 && t <= 42124 || t >= 42128 && t <= 42182 || t >= 43360 && t <= 43388 || t >= 44032 && t <= 55203 || t >= 63744 && t <= 64255 || t >= 65040 && t <= 65049 || t >= 65072 && t <= 65106 || t >= 65108 && t <= 65126 || t >= 65128 && t <= 65131 || t >= 94176 && t <= 94180 || t === 94192 || t === 94193 || t >= 94208 && t <= 100343 || t >= 100352 && t <= 101589 || t >= 101632 && t <= 101640 || t >= 110576 && t <= 110579 || t >= 110581 && t <= 110587 || t === 110589 || t === 110590 || t >= 110592 && t <= 110882 || t === 110898 || t >= 110928 && t <= 110930 || t === 110933 || t >= 110948 && t <= 110951 || t >= 110960 && t <= 111355 || t === 126980 || t === 127183 || t === 127374 || t >= 127377 && t <= 127386 || t >= 127488 && t <= 127490 || t >= 127504 && t <= 127547 || t >= 127552 && t <= 127560 || t === 127568 || t === 127569 || t >= 127584 && t <= 127589 || t >= 127744 && t <= 127776 || t >= 127789 && t <= 127797 || t >= 127799 && t <= 127868 || t >= 127870 && t <= 127891 || t >= 127904 && t <= 127946 || t >= 127951 && t <= 127955 || t >= 127968 && t <= 127984 || t === 127988 || t >= 127992 && t <= 128062 || t === 128064 || t >= 128066 && t <= 128252 || t >= 128255 && t <= 128317 || t >= 128331 && t <= 128334 || t >= 128336 && t <= 128359 || t === 128378 || t === 128405 || t === 128406 || t === 128420 || t >= 128507 && t <= 128591 || t >= 128640 && t <= 128709 || t === 128716 || t >= 128720 && t <= 128722 || t >= 128725 && t <= 128727 || t >= 128732 && t <= 128735 || t === 128747 || t === 128748 || t >= 128756 && t <= 128764 || t >= 128992 && t <= 129003 || t === 129008 || t >= 129292 && t <= 129338 || t >= 129340 && t <= 129349 || t >= 129351 && t <= 129535 || t >= 129648 && t <= 129660 || t >= 129664 && t <= 129672 || t >= 129680 && t <= 129725 || t >= 129727 && t <= 129733 || t >= 129742 && t <= 129755 || t >= 129760 && t <= 129768 || t >= 129776 && t <= 129784 || t >= 131072 && t <= 196605 || t >= 196608 && t <= 262141; -var At2 = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/y; -var it2 = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y; -var nt2 = /\t{1,1000}/y; -var wt = new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*", "yu"); -var at2 = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y; -var Fe = new RegExp("\\p{M}+", "gu"); -var ye = { limit: 1 / 0, ellipsis: "" }; -var jt = (t, r = {}, s = {}) => { - const i = r.limit ?? 1 / 0, a = r.ellipsis ?? "", o = r?.ellipsisWidth ?? (a ? jt(a, ye, s).width : 0), u = s.ansiWidth ?? 0, l = s.controlWidth ?? 0, n = s.tabWidth ?? 8, c = s.ambiguousWidth ?? 1, g = s.emojiWidth ?? 2, F = s.fullWidthWidth ?? 2, p = s.regularWidth ?? 1, E = s.wideWidth ?? 2; - let $ = 0, m = 0, h = t.length, y2 = 0, f = false, v = h, S2 = Math.max(0, i - o), I2 = 0, B2 = 0, A = 0, w = 0; - t: for (; ; ) { - if (B2 > I2 || m >= h && m > $) { - const _2 = t.slice(I2, B2) || t.slice($, m); - y2 = 0; - for (const D2 of _2.replaceAll(Fe, "")) { - const T2 = D2.codePointAt(0) || 0; - if (ge(T2) ? w = F : fe(T2) ? w = E : c !== p && pe(T2) ? w = c : w = p, A + w > S2 && (v = Math.min(v, Math.max(I2, $) + y2)), A + w > i) { - f = true; - break t; - } - y2 += D2.length, A += w; - } - I2 = B2 = 0; - } - if (m >= h) break; - if (at2.lastIndex = m, at2.test(t)) { - if (y2 = at2.lastIndex - m, w = y2 * p, A + w > S2 && (v = Math.min(v, m + Math.floor((S2 - A) / p))), A + w > i) { - f = true; - break; - } - A += w, I2 = $, B2 = m, m = $ = at2.lastIndex; - continue; - } - if (At2.lastIndex = m, At2.test(t)) { - if (A + u > S2 && (v = Math.min(v, m)), A + u > i) { - f = true; - break; - } - A += u, I2 = $, B2 = m, m = $ = At2.lastIndex; - continue; - } - if (it2.lastIndex = m, it2.test(t)) { - if (y2 = it2.lastIndex - m, w = y2 * l, A + w > S2 && (v = Math.min(v, m + Math.floor((S2 - A) / l))), A + w > i) { - f = true; - break; - } - A += w, I2 = $, B2 = m, m = $ = it2.lastIndex; - continue; - } - if (nt2.lastIndex = m, nt2.test(t)) { - if (y2 = nt2.lastIndex - m, w = y2 * n, A + w > S2 && (v = Math.min(v, m + Math.floor((S2 - A) / n))), A + w > i) { - f = true; - break; - } - A += w, I2 = $, B2 = m, m = $ = nt2.lastIndex; - continue; - } - if (wt.lastIndex = m, wt.test(t)) { - if (A + g > S2 && (v = Math.min(v, m)), A + g > i) { - f = true; - break; - } - A += g, I2 = $, B2 = m, m = $ = wt.lastIndex; - continue; - } - m += 1; - } - return { width: f ? S2 : A, index: f ? v : h, truncated: f, ellipsed: f && i >= o }; -}; -var Ee = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; -var M2 = (t, r = {}) => jt(t, Ee, r).width; -var ot2 = "\x1B"; -var Gt = "\x9B"; -var ve = 39; -var Ct2 = "\x07"; -var kt2 = "["; -var Ae = "]"; -var Vt2 = "m"; -var St = `${Ae}8;;`; -var Ht = new RegExp(`(?:\\${kt2}(?\\d+)m|\\${St}(?.*)${Ct2})`, "y"); -var we = (t) => { - if (t >= 30 && t <= 37 || t >= 90 && t <= 97) return 39; - if (t >= 40 && t <= 47 || t >= 100 && t <= 107) return 49; - if (t === 1 || t === 2) return 22; - if (t === 3) return 23; - if (t === 4) return 24; - if (t === 7) return 27; - if (t === 8) return 28; - if (t === 9) return 29; - if (t === 0) return 0; -}; -var Ut = (t) => `${ot2}${kt2}${t}${Vt2}`; -var Kt = (t) => `${ot2}${St}${t}${Ct2}`; -var Ce = (t) => t.map((r) => M2(r)); -var It2 = (t, r, s) => { - const i = r[Symbol.iterator](); - let a = false, o = false, u = t.at(-1), l = u === void 0 ? 0 : M2(u), n = i.next(), c = i.next(), g = 0; - for (; !n.done; ) { - const F = n.value, p = M2(F); - l + p <= s ? t[t.length - 1] += F : (t.push(F), l = 0), (F === ot2 || F === Gt) && (a = true, o = r.startsWith(St, g + 1)), a ? o ? F === Ct2 && (a = false, o = false) : F === Vt2 && (a = false) : (l += p, l === s && !c.done && (t.push(""), l = 0)), n = c, c = i.next(), g += F.length; - } - u = t.at(-1), !l && u !== void 0 && u.length > 0 && t.length > 1 && (t[t.length - 2] += t.pop()); -}; -var Se = (t) => { - const r = t.split(" "); - let s = r.length; - for (; s > 0 && !(M2(r[s - 1]) > 0); ) s--; - return s === r.length ? t : r.slice(0, s).join(" ") + r.slice(s).join(""); -}; -var Ie = (t, r, s = {}) => { - if (s.trim !== false && t.trim() === "") return ""; - let i = "", a, o; - const u = t.split(" "), l = Ce(u); - let n = [""]; - for (const [$, m] of u.entries()) { - s.trim !== false && (n[n.length - 1] = (n.at(-1) ?? "").trimStart()); - let h = M2(n.at(-1) ?? ""); - if ($ !== 0 && (h >= r && (s.wordWrap === false || s.trim === false) && (n.push(""), h = 0), (h > 0 || s.trim === false) && (n[n.length - 1] += " ", h++)), s.hard && l[$] > r) { - const y2 = r - h, f = 1 + Math.floor((l[$] - y2 - 1) / r); - Math.floor((l[$] - 1) / r) < f && n.push(""), It2(n, m, r); - continue; - } - if (h + l[$] > r && h > 0 && l[$] > 0) { - if (s.wordWrap === false && h < r) { - It2(n, m, r); - continue; - } - n.push(""); - } - if (h + l[$] > r && s.wordWrap === false) { - It2(n, m, r); - continue; - } - n[n.length - 1] += m; - } - s.trim !== false && (n = n.map(($) => Se($))); - const c = n.join(` -`), g = c[Symbol.iterator](); - let F = g.next(), p = g.next(), E = 0; - for (; !F.done; ) { - const $ = F.value, m = p.value; - if (i += $, $ === ot2 || $ === Gt) { - Ht.lastIndex = E + 1; - const f = Ht.exec(c)?.groups; - if (f?.code !== void 0) { - const v = Number.parseFloat(f.code); - a = v === ve ? void 0 : v; - } else f?.uri !== void 0 && (o = f.uri.length === 0 ? void 0 : f.uri); - } - const h = a ? we(a) : void 0; - m === ` -` ? (o && (i += Kt("")), a && h && (i += Ut(h))) : $ === ` -` && (a && h && (i += Ut(a)), o && (i += Kt(o))), E += $.length, F = p, p = g.next(); - } - return i; -}; -function J2(t, r, s) { - return String(t).normalize().replaceAll(`\r -`, ` -`).split(` -`).map((i) => Ie(i, r, s)).join(` -`); -} -var be = (t, r, s, i, a) => { - let o = r, u = 0; - for (let l = s; l < i; l++) { - const n = t[l]; - if (o = o - n.length, u++, o <= a) break; - } - return { lineCount: o, removals: u }; -}; -var X2 = (t) => { - const { cursor: r, options: s, style: i } = t, a = t.output ?? process.stdout, o = rt(a), u = t.columnPadding ?? 0, l = t.rowPadding ?? 4, n = o - u, c = nt(a), g = import_picocolors2.default.dim("..."), F = t.maxItems ?? Number.POSITIVE_INFINITY, p = Math.max(c - l, 0), E = Math.max(Math.min(F, p), 5); - let $ = 0; - r >= E - 3 && ($ = Math.max(Math.min(r - E + 3, s.length - E), 0)); - let m = E < s.length && $ > 0, h = E < s.length && $ + E < s.length; - const y2 = Math.min($ + E, s.length), f = []; - let v = 0; - m && v++, h && v++; - const S2 = $ + (m ? 1 : 0), I2 = y2 - (h ? 1 : 0); - for (let A = S2; A < I2; A++) { - const w = J2(i(s[A], A === r), n, { hard: true, trim: false }).split(` -`); - f.push(w), v += w.length; - } - if (v > p) { - let A = 0, w = 0, _2 = v; - const D2 = r - S2, T2 = (Y, L2) => be(f, _2, Y, L2, p); - m ? ({ lineCount: _2, removals: A } = T2(0, D2), _2 > p && ({ lineCount: _2, removals: w } = T2(D2 + 1, f.length))) : ({ lineCount: _2, removals: w } = T2(D2 + 1, f.length), _2 > p && ({ lineCount: _2, removals: A } = T2(0, D2))), A > 0 && (m = true, f.splice(0, A)), w > 0 && (h = true, f.splice(f.length - w, w)); - } - const B2 = []; - m && B2.push(g); - for (const A of f) for (const w of A) B2.push(w); - return h && B2.push(g), B2; -}; -var Re = (t) => { - const r = t.active ?? "Yes", s = t.inactive ?? "No"; - return new kt({ active: r, inactive: s, signal: t.signal, input: t.input, output: t.output, initialValue: t.initialValue ?? true, render() { - const i = t.withGuide ?? _.withGuide, a = `${i ? `${import_picocolors2.default.gray(d)} -` : ""}${W2(this.state)} ${t.message} -`, o = this.value ? r : s; - switch (this.state) { - case "submit": { - const u = i ? `${import_picocolors2.default.gray(d)} ` : ""; - return `${a}${u}${import_picocolors2.default.dim(o)}`; - } - case "cancel": { - const u = i ? `${import_picocolors2.default.gray(d)} ` : ""; - return `${a}${u}${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o))}${i ? ` -${import_picocolors2.default.gray(d)}` : ""}`; - } - default: { - const u = i ? `${import_picocolors2.default.cyan(d)} ` : "", l = i ? import_picocolors2.default.cyan(x2) : ""; - return `${a}${u}${this.value ? `${import_picocolors2.default.green(Q2)} ${r}` : `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(r)}`}${t.vertical ? i ? ` -${import_picocolors2.default.cyan(d)} ` : ` -` : ` ${import_picocolors2.default.dim("/")} `}${this.value ? `${import_picocolors2.default.dim(H2)} ${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.green(Q2)} ${s}`} -${l} -`; - } - } - } }).prompt(); -}; -var R2 = { message: (t = [], { symbol: r = import_picocolors2.default.gray(d), secondarySymbol: s = import_picocolors2.default.gray(d), output: i = process.stdout, spacing: a = 1, withGuide: o } = {}) => { - const u = [], l = o ?? _.withGuide, n = l ? s : "", c = l ? `${r} ` : "", g = l ? `${s} ` : ""; - for (let p = 0; p < a; p++) u.push(n); - const F = Array.isArray(t) ? t : t.split(` -`); - if (F.length > 0) { - const [p, ...E] = F; - p.length > 0 ? u.push(`${c}${p}`) : u.push(l ? r : ""); - for (const $ of E) $.length > 0 ? u.push(`${g}${$}`) : u.push(l ? s : ""); - } - i.write(`${u.join(` -`)} -`); -}, info: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.blue(ft2) }); -}, success: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.green(Ft2) }); -}, step: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.green(V) }); -}, warn: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.yellow(yt2) }); -}, warning: (t, r) => { - R2.warn(t, r); -}, error: (t, r) => { - R2.message(t, { ...r, symbol: import_picocolors2.default.red(Et2) }); -} }; -var Ne = (t = "", r) => { - (r?.output ?? process.stdout).write(`${import_picocolors2.default.gray(x2)} ${import_picocolors2.default.red(t)} - -`); -}; -var We = (t = "", r) => { - (r?.output ?? process.stdout).write(`${import_picocolors2.default.gray(ht2)} ${t} -`); -}; -var Le = (t = "", r) => { - (r?.output ?? process.stdout).write(`${import_picocolors2.default.gray(d)} -${import_picocolors2.default.gray(x2)} ${t} - -`); -}; -var Z2 = (t, r) => t.split(` -`).map((s) => r(s)).join(` -`); -var je = (t) => { - const r = (i, a) => { - const o = i.label ?? String(i.value); - return a === "disabled" ? `${import_picocolors2.default.gray(q2)} ${Z2(o, (u) => import_picocolors2.default.strikethrough(import_picocolors2.default.gray(u)))}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint ?? "disabled"})`)}` : ""}` : a === "active" ? `${import_picocolors2.default.cyan(st2)} ${o}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : a === "selected" ? `${import_picocolors2.default.green(U2)} ${Z2(o, import_picocolors2.default.dim)}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : a === "cancelled" ? `${Z2(o, (u) => import_picocolors2.default.strikethrough(import_picocolors2.default.dim(u)))}` : a === "active-selected" ? `${import_picocolors2.default.green(U2)} ${o}${i.hint ? ` ${import_picocolors2.default.dim(`(${i.hint})`)}` : ""}` : a === "submitted" ? `${Z2(o, import_picocolors2.default.dim)}` : `${import_picocolors2.default.dim(q2)} ${Z2(o, import_picocolors2.default.dim)}`; - }, s = t.required ?? true; - return new Lt({ options: t.options, signal: t.signal, input: t.input, output: t.output, initialValues: t.initialValues, required: s, cursorAt: t.cursorAt, validate(i) { - if (s && (i === void 0 || i.length === 0)) return `Please select at least one option. -${import_picocolors2.default.reset(import_picocolors2.default.dim(`Press ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" space ")))} to select, ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" enter ")))} to submit`))}`; - }, render() { - const i = xt(t.output, t.message, `${vt2(this.state)} `, `${W2(this.state)} `), a = `${import_picocolors2.default.gray(d)} -${i} -`, o = this.value ?? [], u = (l, n) => { - if (l.disabled) return r(l, "disabled"); - const c = o.includes(l.value); - return n && c ? r(l, "active-selected") : c ? r(l, "selected") : r(l, n ? "active" : "inactive"); - }; - switch (this.state) { - case "submit": { - const l = this.options.filter(({ value: c }) => o.includes(c)).map((c) => r(c, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none"), n = xt(t.output, l, `${import_picocolors2.default.gray(d)} `); - return `${a}${n}`; - } - case "cancel": { - const l = this.options.filter(({ value: c }) => o.includes(c)).map((c) => r(c, "cancelled")).join(import_picocolors2.default.dim(", ")); - if (l.trim() === "") return `${a}${import_picocolors2.default.gray(d)}`; - const n = xt(t.output, l, `${import_picocolors2.default.gray(d)} `); - return `${a}${n} -${import_picocolors2.default.gray(d)}`; - } - case "error": { - const l = `${import_picocolors2.default.yellow(d)} `, n = this.error.split(` -`).map((F, p) => p === 0 ? `${import_picocolors2.default.yellow(x2)} ${import_picocolors2.default.yellow(F)}` : ` ${F}`).join(` -`), c = a.split(` -`).length, g = n.split(` -`).length + 1; - return `${a}${l}${X2({ output: t.output, options: this.options, cursor: this.cursor, maxItems: t.maxItems, columnPadding: l.length, rowPadding: c + g, style: u }).join(` -${l}`)} -${n} -`; - } - default: { - const l = `${import_picocolors2.default.cyan(d)} `, n = a.split(` -`).length; - return `${a}${l}${X2({ output: t.output, options: this.options, cursor: this.cursor, maxItems: t.maxItems, columnPadding: l.length, rowPadding: n + 2, style: u }).join(` -${l}`)} -${import_picocolors2.default.cyan(x2)} -`; - } - } - } }).prompt(); -}; -var Ge = (t) => import_picocolors2.default.dim(t); -var ke = (t, r, s) => { - const i = { hard: true, trim: false }, a = J2(t, r, i).split(` -`), o = a.reduce((n, c) => Math.max(M2(c), n), 0), u = a.map(s).reduce((n, c) => Math.max(M2(c), n), 0), l = r - (u - o); - return J2(t, l, i); -}; -var Ve = (t = "", r = "", s) => { - const i = s?.output ?? N2.stdout, a = s?.withGuide ?? _.withGuide, o = s?.format ?? Ge, u = ["", ...ke(t, rt(i) - 6, o).split(` -`).map(o), ""], l = M2(r), n = Math.max(u.reduce((p, E) => { - const $ = M2(E); - return $ > p ? $ : p; - }, 0), l) + 2, c = u.map((p) => `${import_picocolors2.default.gray(d)} ${p}${" ".repeat(n - M2(p))}${import_picocolors2.default.gray(d)}`).join(` -`), g = a ? `${import_picocolors2.default.gray(d)} -` : "", F = a ? Wt2 : gt2; - i.write(`${g}${import_picocolors2.default.green(V)} ${import_picocolors2.default.reset(r)} ${import_picocolors2.default.gray(rt2.repeat(Math.max(n - l - 1, 1)) + mt2)} -${c} -${import_picocolors2.default.gray(F + rt2.repeat(n + 2) + pt2)} -`); -}; -var He = (t) => new Mt({ validate: t.validate, mask: t.mask ?? Nt, signal: t.signal, input: t.input, output: t.output, render() { - const r = t.withGuide ?? _.withGuide, s = `${r ? `${import_picocolors2.default.gray(d)} -` : ""}${W2(this.state)} ${t.message} -`, i = this.userInputWithCursor, a = this.masked; - switch (this.state) { - case "error": { - const o = r ? `${import_picocolors2.default.yellow(d)} ` : "", u = r ? `${import_picocolors2.default.yellow(x2)} ` : "", l = a ?? ""; - return t.clearOnError && this.clear(), `${s.trim()} -${o}${l} -${u}${import_picocolors2.default.yellow(this.error)} -`; - } - case "submit": { - const o = r ? `${import_picocolors2.default.gray(d)} ` : "", u = a ? import_picocolors2.default.dim(a) : ""; - return `${s}${o}${u}`; - } - case "cancel": { - const o = r ? `${import_picocolors2.default.gray(d)} ` : "", u = a ? import_picocolors2.default.strikethrough(import_picocolors2.default.dim(a)) : ""; - return `${s}${o}${u}${a && r ? ` -${import_picocolors2.default.gray(d)}` : ""}`; - } - default: { - const o = r ? `${import_picocolors2.default.cyan(d)} ` : "", u = r ? import_picocolors2.default.cyan(x2) : ""; - return `${s}${o}${i} -${u} -`; - } - } -} }).prompt(); -var Ke = import_picocolors2.default.magenta; -var bt2 = ({ indicator: t = "dots", onCancel: r, output: s = process.stdout, cancelMessage: i, errorMessage: a, frames: o = et2 ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: u = et2 ? 80 : 120, signal: l, ...n } = {}) => { - const c = ct2(); - let g, F, p = false, E = false, $ = "", m, h = performance.now(); - const y2 = rt(s), f = n?.styleFrame ?? Ke, v = (b) => { - const O2 = b > 1 ? a ?? _.messages.error : i ?? _.messages.cancel; - E = b === 1, p && (L2(O2, b), E && typeof r == "function" && r()); - }, S2 = () => v(2), I2 = () => v(1), B2 = () => { - process.on("uncaughtExceptionMonitor", S2), process.on("unhandledRejection", S2), process.on("SIGINT", I2), process.on("SIGTERM", I2), process.on("exit", v), l && l.addEventListener("abort", I2); - }, A = () => { - process.removeListener("uncaughtExceptionMonitor", S2), process.removeListener("unhandledRejection", S2), process.removeListener("SIGINT", I2), process.removeListener("SIGTERM", I2), process.removeListener("exit", v), l && l.removeEventListener("abort", I2); - }, w = () => { - if (m === void 0) return; - c && s.write(` -`); - const b = J2(m, y2, { hard: true, trim: false }).split(` -`); - b.length > 1 && s.write(import_sisteransi2.cursor.up(b.length - 1)), s.write(import_sisteransi2.cursor.to(0)), s.write(import_sisteransi2.erase.down()); - }, _2 = (b) => b.replace(/\.+$/, ""), D2 = (b) => { - const O2 = (performance.now() - b) / 1e3, j2 = Math.floor(O2 / 60), G2 = Math.floor(O2 % 60); - return j2 > 0 ? `[${j2}m ${G2}s]` : `[${G2}s]`; - }, T2 = n.withGuide ?? _.withGuide, Y = (b = "") => { - p = true, g = Bt({ output: s }), $ = _2(b), h = performance.now(), T2 && s.write(`${import_picocolors2.default.gray(d)} -`); - let O2 = 0, j2 = 0; - B2(), F = setInterval(() => { - if (c && $ === m) return; - w(), m = $; - const G2 = f(o[O2]); - let tt2; - if (c) tt2 = `${G2} ${$}...`; - else if (t === "timer") tt2 = `${G2} ${$} ${D2(h)}`; - else { - const te = ".".repeat(Math.floor(j2)).slice(0, 3); - tt2 = `${G2} ${$}${te}`; - } - const Zt = J2(tt2, y2, { hard: true, trim: false }); - s.write(Zt), O2 = O2 + 1 < o.length ? O2 + 1 : 0, j2 = j2 < 4 ? j2 + 0.125 : 0; - }, u); - }, L2 = (b = "", O2 = 0, j2 = false) => { - if (!p) return; - p = false, clearInterval(F), w(); - const G2 = O2 === 0 ? import_picocolors2.default.green(V) : O2 === 1 ? import_picocolors2.default.red(dt2) : import_picocolors2.default.red($t2); - $ = b ?? $, j2 || (t === "timer" ? s.write(`${G2} ${$} ${D2(h)} -`) : s.write(`${G2} ${$} -`)), A(), g(); - }; - return { start: Y, stop: (b = "") => L2(b, 0), message: (b = "") => { - $ = _2(b ?? $); - }, cancel: (b = "") => L2(b, 1), error: (b = "") => L2(b, 2), clear: () => L2("", 0, true), get isCancelled() { - return E; - } }; -}; -var zt = { light: C("\u2500", "-"), heavy: C("\u2501", "="), block: C("\u2588", "#") }; -var lt2 = (t, r) => t.includes(` -`) ? t.split(` -`).map((s) => r(s)).join(` -`) : r(t); -var Je = (t) => { - const r = (s, i) => { - const a = s.label ?? String(s.value); - switch (i) { - case "disabled": - return `${import_picocolors2.default.gray(H2)} ${lt2(a, import_picocolors2.default.gray)}${s.hint ? ` ${import_picocolors2.default.dim(`(${s.hint ?? "disabled"})`)}` : ""}`; - case "selected": - return `${lt2(a, import_picocolors2.default.dim)}`; - case "active": - return `${import_picocolors2.default.green(Q2)} ${a}${s.hint ? ` ${import_picocolors2.default.dim(`(${s.hint})`)}` : ""}`; - case "cancelled": - return `${lt2(a, (o) => import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o)))}`; - default: - return `${import_picocolors2.default.dim(H2)} ${lt2(a, import_picocolors2.default.dim)}`; - } - }; - return new Wt({ options: t.options, signal: t.signal, input: t.input, output: t.output, initialValue: t.initialValue, render() { - const s = t.withGuide ?? _.withGuide, i = `${W2(this.state)} `, a = `${vt2(this.state)} `, o = xt(t.output, t.message, a, i), u = `${s ? `${import_picocolors2.default.gray(d)} -` : ""}${o} -`; - switch (this.state) { - case "submit": { - const l = s ? `${import_picocolors2.default.gray(d)} ` : "", n = xt(t.output, r(this.options[this.cursor], "selected"), l); - return `${u}${n}`; - } - case "cancel": { - const l = s ? `${import_picocolors2.default.gray(d)} ` : "", n = xt(t.output, r(this.options[this.cursor], "cancelled"), l); - return `${u}${n}${s ? ` -${import_picocolors2.default.gray(d)}` : ""}`; - } - default: { - const l = s ? `${import_picocolors2.default.cyan(d)} ` : "", n = s ? import_picocolors2.default.cyan(x2) : "", c = u.split(` -`).length, g = s ? 2 : 1; - return `${u}${l}${X2({ output: t.output, cursor: this.cursor, options: this.options, maxItems: t.maxItems, columnPadding: l.length, rowPadding: c + g, style: (F, p) => r(F, F.disabled ? "disabled" : p ? "active" : "inactive") }).join(` -${l}`)} -${n} -`; - } - } - } }).prompt(); -}; -var Qt = `${import_picocolors2.default.gray(d)} `; -var Ye = async (t, r) => { - for (const s of t) { - if (s.enabled === false) continue; - const i = bt2(r); - i.start(s.title); - const a = await s.task(i.message); - i.stop(a || s.title); - } -}; -var Ze = (t) => new $t({ validate: t.validate, placeholder: t.placeholder, defaultValue: t.defaultValue, initialValue: t.initialValue, output: t.output, signal: t.signal, input: t.input, render() { - const r = t?.withGuide ?? _.withGuide, s = `${`${r ? `${import_picocolors2.default.gray(d)} -` : ""}${W2(this.state)} `}${t.message} -`, i = t.placeholder ? import_picocolors2.default.inverse(t.placeholder[0]) + import_picocolors2.default.dim(t.placeholder.slice(1)) : import_picocolors2.default.inverse(import_picocolors2.default.hidden("_")), a = this.userInput ? this.userInputWithCursor : i, o = this.value ?? ""; - switch (this.state) { - case "error": { - const u = this.error ? ` ${import_picocolors2.default.yellow(this.error)}` : "", l = r ? `${import_picocolors2.default.yellow(d)} ` : "", n = r ? import_picocolors2.default.yellow(x2) : ""; - return `${s.trim()} -${l}${a} -${n}${u} -`; - } - case "submit": { - const u = o ? ` ${import_picocolors2.default.dim(o)}` : "", l = r ? import_picocolors2.default.gray(d) : ""; - return `${s}${l}${u}`; - } - case "cancel": { - const u = o ? ` ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(o))}` : "", l = r ? import_picocolors2.default.gray(d) : ""; - return `${s}${l}${u}${o.trim() ? ` -${l}` : ""}`; - } - default: { - const u = r ? `${import_picocolors2.default.cyan(d)} ` : "", l = r ? import_picocolors2.default.cyan(x2) : ""; - return `${s}${u}${a} -${l} -`; - } - } -} }).prompt(); - -// src/steps/welcome.ts -var import_picocolors3 = __toESM(require_picocolors(), 1); -import { existsSync } from "fs"; - -// src/utils/system.ts -import { execSync } from "child_process"; -import { homedir } from "os"; -import { join } from "path"; -function detectOS() { - switch (process.platform) { - case "darwin": - return "macos"; - case "win32": - return "windows"; - default: - return "linux"; - } -} -function commandExists(command) { - try { - execSync(`which ${command}`, { stdio: "pipe" }); - return true; - } catch { - return false; - } -} -function runCommand(command, args = []) { - try { - const fullCommand = [command, ...args].join(" "); - const stdout = execSync(fullCommand, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); - return { stdout: stdout.trim(), stderr: "", exitCode: 0 }; - } catch (error) { - return { - stdout: error.stdout?.toString().trim() ?? "", - stderr: error.stderr?.toString().trim() ?? "", - exitCode: error.status ?? 1 - }; - } -} -function expandHome(filepath) { - if (filepath.startsWith("~")) { - return join(homedir(), filepath.slice(1)); - } - return filepath; -} - -// src/steps/welcome.ts -async function runWelcome() { - We(import_picocolors3.default.bgCyan(import_picocolors3.default.black(" claude-mem installer "))); - R2.info(`Version: 1.0.0`); - R2.info(`Platform: ${process.platform} (${process.arch})`); - const settingsExist = existsSync(expandHome("~/.claude-mem/settings.json")); - const pluginExist = existsSync(expandHome("~/.claude/plugins/marketplaces/thedotmack/")); - const alreadyInstalled = settingsExist && pluginExist; - if (alreadyInstalled) { - R2.warn("Existing claude-mem installation detected."); - } - const installMode = await Je({ - message: "What would you like to do?", - options: alreadyInstalled ? [ - { value: "upgrade", label: "Upgrade", hint: "update to latest version" }, - { value: "configure", label: "Configure", hint: "change settings only" }, - { value: "fresh", label: "Fresh Install", hint: "reinstall from scratch" } - ] : [ - { value: "fresh", label: "Fresh Install", hint: "recommended" }, - { value: "configure", label: "Configure Only", hint: "set up settings without installing" } - ] - }); - if (Ct(installMode)) { - Ne("Installation cancelled."); - process.exit(0); - } - return installMode; -} - -// src/steps/dependencies.ts -var import_picocolors4 = __toESM(require_picocolors(), 1); - -// src/utils/dependencies.ts -import { existsSync as existsSync2 } from "fs"; -import { execSync as execSync2 } from "child_process"; -function findBinary(name, extraPaths = []) { - if (commandExists(name)) { - const result = runCommand("which", [name]); - const versionResult = runCommand(name, ["--version"]); - return { - found: true, - path: result.stdout, - version: parseVersion(versionResult.stdout) || parseVersion(versionResult.stderr) - }; - } - for (const extraPath of extraPaths) { - const fullPath = expandHome(extraPath); - if (existsSync2(fullPath)) { - const versionResult = runCommand(fullPath, ["--version"]); - return { - found: true, - path: fullPath, - version: parseVersion(versionResult.stdout) || parseVersion(versionResult.stderr) - }; - } - } - return { found: false, path: null, version: null }; -} -function parseVersion(output) { - if (!output) return null; - const match = output.match(/(\d+\.\d+(\.\d+)?)/); - return match ? match[1] : null; -} -function compareVersions(current, minimum) { - const currentParts = current.split(".").map(Number); - const minimumParts = minimum.split(".").map(Number); - for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i++) { - const a = currentParts[i] || 0; - const b = minimumParts[i] || 0; - if (a > b) return true; - if (a < b) return false; - } - return true; -} -function installBun() { - const os = detectOS(); - if (os === "windows") { - execSync2('powershell -c "irm bun.sh/install.ps1 | iex"', { stdio: "inherit" }); - } else { - execSync2("curl -fsSL https://bun.sh/install | bash", { stdio: "inherit" }); - } -} -function installUv() { - const os = detectOS(); - if (os === "windows") { - execSync2('powershell -c "irm https://astral.sh/uv/install.ps1 | iex"', { stdio: "inherit" }); - } else { - execSync2("curl -fsSL https://astral.sh/uv/install.sh | sh", { stdio: "inherit" }); - } -} - -// src/steps/dependencies.ts -var BUN_EXTRA_PATHS = ["~/.bun/bin/bun", "/usr/local/bin/bun", "/opt/homebrew/bin/bun"]; -var UV_EXTRA_PATHS = ["~/.local/bin/uv", "~/.cargo/bin/uv"]; -async function runDependencyChecks() { - const status = { - nodeOk: false, - gitOk: false, - bunOk: false, - uvOk: false, - bunPath: null, - uvPath: null - }; - await Ye([ - { - title: "Checking Node.js", - task: async () => { - const version = process.version.slice(1); - if (compareVersions(version, "18.0.0")) { - status.nodeOk = true; - return `Node.js ${process.version} ${import_picocolors4.default.green("\u2713")}`; - } - return `Node.js ${process.version} \u2014 requires >= 18.0.0 ${import_picocolors4.default.red("\u2717")}`; - } - }, - { - title: "Checking git", - task: async () => { - const info = findBinary("git"); - if (info.found) { - status.gitOk = true; - return `git ${info.version ?? ""} ${import_picocolors4.default.green("\u2713")}`; - } - return `git not found ${import_picocolors4.default.red("\u2717")}`; - } - }, - { - title: "Checking Bun", - task: async () => { - const info = findBinary("bun", BUN_EXTRA_PATHS); - if (info.found && info.version && compareVersions(info.version, "1.1.14")) { - status.bunOk = true; - status.bunPath = info.path; - return `Bun ${info.version} ${import_picocolors4.default.green("\u2713")}`; - } - if (info.found && info.version) { - return `Bun ${info.version} \u2014 requires >= 1.1.14 ${import_picocolors4.default.yellow("\u26A0")}`; - } - return `Bun not found ${import_picocolors4.default.yellow("\u26A0")}`; - } - }, - { - title: "Checking uv", - task: async () => { - const info = findBinary("uv", UV_EXTRA_PATHS); - if (info.found) { - status.uvOk = true; - status.uvPath = info.path; - return `uv ${info.version ?? ""} ${import_picocolors4.default.green("\u2713")}`; - } - return `uv not found ${import_picocolors4.default.yellow("\u26A0")}`; - } - } - ]); - if (!status.gitOk) { - const os = detectOS(); - R2.error("git is required but not found."); - if (os === "macos") { - R2.info("Install with: xcode-select --install"); - } else if (os === "linux") { - R2.info("Install with: sudo apt install git (or your distro equivalent)"); - } else { - R2.info("Download from: https://git-scm.com/downloads"); - } - Ne("Please install git and try again."); - process.exit(1); - } - if (!status.nodeOk) { - R2.error(`Node.js >= 18.0.0 is required. Current: ${process.version}`); - Ne("Please upgrade Node.js and try again."); - process.exit(1); - } - if (!status.bunOk) { - const shouldInstall = await Re({ - message: "Bun is required but not found. Install it now?", - initialValue: true - }); - if (Ct(shouldInstall)) { - Ne("Installation cancelled."); - process.exit(0); - } - if (shouldInstall) { - const s = bt2(); - s.start("Installing Bun..."); - try { - installBun(); - const recheck = findBinary("bun", BUN_EXTRA_PATHS); - if (recheck.found) { - status.bunOk = true; - status.bunPath = recheck.path; - s.stop(`Bun installed ${import_picocolors4.default.green("\u2713")}`); - } else { - s.stop(`Bun installed but not found in PATH. You may need to restart your shell.`); - } - } catch { - s.stop(`Bun installation failed. Install manually: curl -fsSL https://bun.sh/install | bash`); - } - } else { - R2.warn("Bun is required for claude-mem. Install manually: curl -fsSL https://bun.sh/install | bash"); - Ne("Cannot continue without Bun."); - process.exit(1); - } - } - if (!status.uvOk) { - const shouldInstall = await Re({ - message: "uv (Python package manager) is recommended for Chroma. Install it now?", - initialValue: true - }); - if (Ct(shouldInstall)) { - Ne("Installation cancelled."); - process.exit(0); - } - if (shouldInstall) { - const s = bt2(); - s.start("Installing uv..."); - try { - installUv(); - const recheck = findBinary("uv", UV_EXTRA_PATHS); - if (recheck.found) { - status.uvOk = true; - status.uvPath = recheck.path; - s.stop(`uv installed ${import_picocolors4.default.green("\u2713")}`); - } else { - s.stop("uv installed but not found in PATH. You may need to restart your shell."); - } - } catch { - s.stop("uv installation failed. Install manually: curl -fsSL https://astral.sh/uv/install.sh | sh"); - } - } else { - R2.warn("Skipping uv \u2014 Chroma vector search will not be available."); - } - } - return status; -} - -// src/steps/ide-selection.ts -async function runIdeSelection() { - const result = await je({ - message: "Which IDEs do you use?", - options: [ - { value: "claude-code", label: "Claude Code", hint: "recommended" }, - { value: "cursor", label: "Cursor" } - // Windsurf coming soon - not yet selectable - ], - initialValues: ["claude-code"], - required: true - }); - if (Ct(result)) { - Ne("Installation cancelled."); - process.exit(0); - } - const selectedIDEs = result; - if (selectedIDEs.includes("claude-code")) { - R2.info("Claude Code: Plugin will be registered via marketplace."); - } - if (selectedIDEs.includes("cursor")) { - R2.info("Cursor: Hooks will be configured for your projects."); - } - return selectedIDEs; -} - -// src/steps/provider.ts -async function runProviderConfiguration() { - const provider = await Je({ - message: "Which AI provider should claude-mem use for memory compression?", - options: [ - { value: "claude", label: "Claude", hint: "uses your Claude subscription" }, - { value: "gemini", label: "Gemini", hint: "free tier available" }, - { value: "openrouter", label: "OpenRouter", hint: "free models available" } - ] - }); - if (Ct(provider)) { - Ne("Installation cancelled."); - process.exit(0); - } - const config = { provider }; - if (provider === "claude") { - const authMethod = await Je({ - message: "How should Claude authenticate?", - options: [ - { value: "cli", label: "CLI (Max Plan subscription)", hint: "no API key needed" }, - { value: "api", label: "API Key", hint: "uses Anthropic API credits" } - ] - }); - if (Ct(authMethod)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.claudeAuthMethod = authMethod; - if (authMethod === "api") { - const apiKey = await He({ - message: "Enter your Anthropic API key:", - validate: (value) => { - if (!value || value.trim().length === 0) return "API key is required"; - if (!value.startsWith("sk-ant-")) return "Anthropic API keys start with sk-ant-"; - } - }); - if (Ct(apiKey)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.apiKey = apiKey; - } - } - if (provider === "gemini") { - const apiKey = await He({ - message: "Enter your Gemini API key:", - validate: (value) => { - if (!value || value.trim().length === 0) return "API key is required"; - } - }); - if (Ct(apiKey)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.apiKey = apiKey; - const model = await Je({ - message: "Which Gemini model?", - options: [ - { value: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite", hint: "fastest, highest free RPM" }, - { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", hint: "balanced" }, - { value: "gemini-3-flash-preview", label: "Gemini 3 Flash Preview", hint: "latest" } - ] - }); - if (Ct(model)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.model = model; - const rateLimiting = await Re({ - message: "Enable rate limiting? (recommended for free tier)", - initialValue: true - }); - if (Ct(rateLimiting)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.rateLimitingEnabled = rateLimiting; - } - if (provider === "openrouter") { - const apiKey = await He({ - message: "Enter your OpenRouter API key:", - validate: (value) => { - if (!value || value.trim().length === 0) return "API key is required"; - } - }); - if (Ct(apiKey)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.apiKey = apiKey; - const model = await Ze({ - message: "Which OpenRouter model?", - defaultValue: "xiaomi/mimo-v2-flash:free", - placeholder: "xiaomi/mimo-v2-flash:free" - }); - if (Ct(model)) { - Ne("Installation cancelled."); - process.exit(0); - } - config.model = model; - } - return config; -} - -// src/steps/settings.ts -var import_picocolors5 = __toESM(require_picocolors(), 1); -async function runSettingsConfiguration() { - const useDefaults = await Re({ - message: "Use default settings? (recommended for most users)", - initialValue: true - }); - if (Ct(useDefaults)) { - Ne("Installation cancelled."); - process.exit(0); - } - if (useDefaults) { - return { - workerPort: "37777", - dataDir: "~/.claude-mem", - contextObservations: "50", - logLevel: "INFO", - pythonVersion: "3.13", - chromaEnabled: true, - chromaMode: "local" - }; - } - const workerPort = await Ze({ - message: "Worker service port:", - defaultValue: "37777", - placeholder: "37777", - validate: (value = "") => { - const port = parseInt(value, 10); - if (isNaN(port) || port < 1024 || port > 65535) { - return "Port must be between 1024 and 65535"; - } - } - }); - if (Ct(workerPort)) { - Ne("Installation cancelled."); - process.exit(0); - } - const dataDir = await Ze({ - message: "Data directory:", - defaultValue: "~/.claude-mem", - placeholder: "~/.claude-mem" - }); - if (Ct(dataDir)) { - Ne("Installation cancelled."); - process.exit(0); - } - const contextObservations = await Ze({ - message: "Number of context observations per session:", - defaultValue: "50", - placeholder: "50", - validate: (value = "") => { - const num = parseInt(value, 10); - if (isNaN(num) || num < 1 || num > 200) { - return "Must be between 1 and 200"; - } - } - }); - if (Ct(contextObservations)) { - Ne("Installation cancelled."); - process.exit(0); - } - const logLevel = await Je({ - message: "Log level:", - options: [ - { value: "DEBUG", label: "DEBUG", hint: "verbose" }, - { value: "INFO", label: "INFO", hint: "default" }, - { value: "WARN", label: "WARN" }, - { value: "ERROR", label: "ERROR", hint: "errors only" } - ], - initialValue: "INFO" - }); - if (Ct(logLevel)) { - Ne("Installation cancelled."); - process.exit(0); - } - const pythonVersion = await Ze({ - message: "Python version (for Chroma):", - defaultValue: "3.13", - placeholder: "3.13" - }); - if (Ct(pythonVersion)) { - Ne("Installation cancelled."); - process.exit(0); - } - const chromaEnabled = await Re({ - message: "Enable Chroma vector search?", - initialValue: true - }); - if (Ct(chromaEnabled)) { - Ne("Installation cancelled."); - process.exit(0); - } - let chromaMode; - let chromaHost; - let chromaPort; - let chromaSsl; - if (chromaEnabled) { - const mode = await Je({ - message: "Chroma mode:", - options: [ - { value: "local", label: "Local", hint: "starts local Chroma server" }, - { value: "remote", label: "Remote", hint: "connect to existing server" } - ] - }); - if (Ct(mode)) { - Ne("Installation cancelled."); - process.exit(0); - } - chromaMode = mode; - if (mode === "remote") { - const host = await Ze({ - message: "Chroma host:", - defaultValue: "127.0.0.1", - placeholder: "127.0.0.1" - }); - if (Ct(host)) { - Ne("Installation cancelled."); - process.exit(0); - } - chromaHost = host; - const port = await Ze({ - message: "Chroma port:", - defaultValue: "8000", - placeholder: "8000", - validate: (value = "") => { - const portNum = parseInt(value, 10); - if (isNaN(portNum) || portNum < 1 || portNum > 65535) return "Port must be between 1 and 65535"; - } - }); - if (Ct(port)) { - Ne("Installation cancelled."); - process.exit(0); - } - chromaPort = port; - const ssl = await Re({ - message: "Use SSL for Chroma connection?", - initialValue: false - }); - if (Ct(ssl)) { - Ne("Installation cancelled."); - process.exit(0); - } - chromaSsl = ssl; - } - } - const config = { - workerPort, - dataDir, - contextObservations, - logLevel, - pythonVersion, - chromaEnabled, - chromaMode, - chromaHost, - chromaPort, - chromaSsl - }; - const summaryLines = [ - `Worker port: ${import_picocolors5.default.cyan(workerPort)}`, - `Data directory: ${import_picocolors5.default.cyan(dataDir)}`, - `Context observations: ${import_picocolors5.default.cyan(contextObservations)}`, - `Log level: ${import_picocolors5.default.cyan(logLevel)}`, - `Python version: ${import_picocolors5.default.cyan(pythonVersion)}`, - `Chroma: ${chromaEnabled ? import_picocolors5.default.green("enabled") : import_picocolors5.default.dim("disabled")}` - ]; - if (chromaEnabled && chromaMode) { - summaryLines.push(`Chroma mode: ${import_picocolors5.default.cyan(chromaMode)}`); - } - Ve(summaryLines.join("\n"), "Settings Summary"); - return config; -} - -// src/utils/settings-writer.ts -import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { join as join2 } from "path"; -import { homedir as homedir2 } from "os"; -function expandDataDir(dataDir) { - if (dataDir.startsWith("~")) { - return join2(homedir2(), dataDir.slice(1)); - } - return dataDir; -} -function buildSettingsObject(providerConfig, settingsConfig) { - const settings = { - CLAUDE_MEM_WORKER_PORT: settingsConfig.workerPort, - CLAUDE_MEM_WORKER_HOST: "127.0.0.1", - CLAUDE_MEM_DATA_DIR: expandDataDir(settingsConfig.dataDir), - CLAUDE_MEM_CONTEXT_OBSERVATIONS: settingsConfig.contextObservations, - CLAUDE_MEM_LOG_LEVEL: settingsConfig.logLevel, - CLAUDE_MEM_PYTHON_VERSION: settingsConfig.pythonVersion, - CLAUDE_MEM_PROVIDER: providerConfig.provider - }; - if (providerConfig.provider === "claude") { - settings.CLAUDE_MEM_CLAUDE_AUTH_METHOD = providerConfig.claudeAuthMethod ?? "cli"; - } - if (providerConfig.provider === "gemini") { - if (providerConfig.apiKey) settings.CLAUDE_MEM_GEMINI_API_KEY = providerConfig.apiKey; - if (providerConfig.model) settings.CLAUDE_MEM_GEMINI_MODEL = providerConfig.model; - settings.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED = providerConfig.rateLimitingEnabled !== false ? "true" : "false"; - } - if (providerConfig.provider === "openrouter") { - if (providerConfig.apiKey) settings.CLAUDE_MEM_OPENROUTER_API_KEY = providerConfig.apiKey; - if (providerConfig.model) settings.CLAUDE_MEM_OPENROUTER_MODEL = providerConfig.model; - } - if (settingsConfig.chromaEnabled) { - settings.CLAUDE_MEM_CHROMA_MODE = settingsConfig.chromaMode ?? "local"; - if (settingsConfig.chromaMode === "remote") { - if (settingsConfig.chromaHost) settings.CLAUDE_MEM_CHROMA_HOST = settingsConfig.chromaHost; - if (settingsConfig.chromaPort) settings.CLAUDE_MEM_CHROMA_PORT = settingsConfig.chromaPort; - if (settingsConfig.chromaSsl !== void 0) settings.CLAUDE_MEM_CHROMA_SSL = String(settingsConfig.chromaSsl); - } - } - return settings; -} -function writeSettings(providerConfig, settingsConfig) { - const dataDir = expandDataDir(settingsConfig.dataDir); - const settingsPath = join2(dataDir, "settings.json"); - if (!existsSync3(dataDir)) { - mkdirSync(dataDir, { recursive: true }); - } - let existingSettings = {}; - if (existsSync3(settingsPath)) { - const raw = readFileSync(settingsPath, "utf-8"); - existingSettings = JSON.parse(raw); - } - const newSettings = buildSettingsObject(providerConfig, settingsConfig); - const merged = { ...existingSettings, ...newSettings }; - writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8"); -} - -// src/steps/install.ts -var import_picocolors6 = __toESM(require_picocolors(), 1); -import { execSync as execSync3 } from "child_process"; -import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, cpSync } from "fs"; -import { join as join3 } from "path"; -import { homedir as homedir3, tmpdir } from "os"; -var MARKETPLACE_DIR = join3(homedir3(), ".claude", "plugins", "marketplaces", "thedotmack"); -var PLUGINS_DIR = join3(homedir3(), ".claude", "plugins"); -var CLAUDE_SETTINGS_PATH = join3(homedir3(), ".claude", "settings.json"); -function ensureDir(directoryPath) { - if (!existsSync4(directoryPath)) { - mkdirSync2(directoryPath, { recursive: true }); - } -} -function readJsonFile(filepath) { - if (!existsSync4(filepath)) return {}; - return JSON.parse(readFileSync2(filepath, "utf-8")); -} -function writeJsonFile(filepath, data) { - ensureDir(join3(filepath, "..")); - writeFileSync2(filepath, JSON.stringify(data, null, 2) + "\n", "utf-8"); -} -function registerMarketplace() { - const knownMarketplacesPath = join3(PLUGINS_DIR, "known_marketplaces.json"); - const knownMarketplaces = readJsonFile(knownMarketplacesPath); - knownMarketplaces["thedotmack"] = { - source: { - source: "github", - repo: "thedotmack/claude-mem" - }, - installLocation: MARKETPLACE_DIR, - lastUpdated: (/* @__PURE__ */ new Date()).toISOString(), - autoUpdate: true - }; - ensureDir(PLUGINS_DIR); - writeJsonFile(knownMarketplacesPath, knownMarketplaces); -} -function registerPlugin(version) { - const installedPluginsPath = join3(PLUGINS_DIR, "installed_plugins.json"); - const installedPlugins = readJsonFile(installedPluginsPath); - if (!installedPlugins.version) installedPlugins.version = 2; - if (!installedPlugins.plugins) installedPlugins.plugins = {}; - const pluginCachePath = join3(PLUGINS_DIR, "cache", "thedotmack", "claude-mem", version); - const now = (/* @__PURE__ */ new Date()).toISOString(); - installedPlugins.plugins["claude-mem@thedotmack"] = [ - { - scope: "user", - installPath: pluginCachePath, - version, - installedAt: now, - lastUpdated: now - } - ]; - writeJsonFile(installedPluginsPath, installedPlugins); - ensureDir(pluginCachePath); - const pluginSourceDir = join3(MARKETPLACE_DIR, "plugin"); - if (existsSync4(pluginSourceDir)) { - cpSync(pluginSourceDir, pluginCachePath, { recursive: true }); - } -} -function enablePluginInClaudeSettings() { - const settings = readJsonFile(CLAUDE_SETTINGS_PATH); - if (!settings.enabledPlugins) settings.enabledPlugins = {}; - settings.enabledPlugins["claude-mem@thedotmack"] = true; - writeJsonFile(CLAUDE_SETTINGS_PATH, settings); -} -function getPluginVersion() { - const pluginJsonPath = join3(MARKETPLACE_DIR, "plugin", ".claude-plugin", "plugin.json"); - if (existsSync4(pluginJsonPath)) { - const pluginJson = JSON.parse(readFileSync2(pluginJsonPath, "utf-8")); - return pluginJson.version ?? "1.0.0"; - } - return "1.0.0"; -} -async function runInstallation(selectedIDEs) { - const tempDir = join3(tmpdir(), `claude-mem-install-${Date.now()}`); - await Ye([ - { - title: "Cloning claude-mem repository", - task: async (message) => { - message("Downloading latest release..."); - execSync3( - `git clone --depth 1 https://github.com/thedotmack/claude-mem.git "${tempDir}"`, - { stdio: "pipe" } - ); - return `Repository cloned ${import_picocolors6.default.green("OK")}`; - } - }, - { - title: "Installing dependencies", - task: async (message) => { - message("Running npm install..."); - execSync3("npm install", { cwd: tempDir, stdio: "pipe" }); - return `Dependencies installed ${import_picocolors6.default.green("OK")}`; - } - }, - { - title: "Building plugin", - task: async (message) => { - message("Compiling TypeScript and bundling..."); - execSync3("npm run build", { cwd: tempDir, stdio: "pipe" }); - return `Plugin built ${import_picocolors6.default.green("OK")}`; - } - }, - { - title: "Registering plugin", - task: async (message) => { - message("Copying files to marketplace directory..."); - ensureDir(MARKETPLACE_DIR); - execSync3( - `rsync -a --delete --exclude=.git --exclude=package-lock.json --exclude=bun.lock "${tempDir}/" "${MARKETPLACE_DIR}/"`, - { stdio: "pipe" } - ); - message("Registering marketplace..."); - registerMarketplace(); - message("Installing marketplace dependencies..."); - execSync3("npm install", { cwd: MARKETPLACE_DIR, stdio: "pipe" }); - message("Registering plugin in Claude Code..."); - const version = getPluginVersion(); - registerPlugin(version); - message("Enabling plugin..."); - enablePluginInClaudeSettings(); - return `Plugin registered (v${getPluginVersion()}) ${import_picocolors6.default.green("OK")}`; - } - } - ]); - try { - execSync3(`rm -rf "${tempDir}"`, { stdio: "pipe" }); - } catch { - } - if (selectedIDEs.includes("cursor")) { - R2.info("Cursor hook configuration will be available after first launch."); - R2.info("Run: claude-mem cursor-setup (coming soon)"); - } -} - -// src/steps/worker.ts -var import_picocolors7 = __toESM(require_picocolors(), 1); -import { spawn } from "child_process"; -import { join as join4 } from "path"; -import { homedir as homedir4 } from "os"; -var MARKETPLACE_DIR2 = join4(homedir4(), ".claude", "plugins", "marketplaces", "thedotmack"); -var HEALTH_CHECK_INTERVAL_MS = 1e3; -var HEALTH_CHECK_MAX_ATTEMPTS = 30; -async function pollHealthEndpoint(port, maxAttempts = HEALTH_CHECK_MAX_ATTEMPTS) { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - try { - const response = await fetch(`http://127.0.0.1:${port}/api/health`); - if (response.ok) return true; - } catch { - } - await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS)); - } - return false; -} -async function runWorkerStartup(workerPort, dataDir) { - const bunInfo = findBinary("bun", ["~/.bun/bin/bun", "/usr/local/bin/bun", "/opt/homebrew/bin/bun"]); - if (!bunInfo.found || !bunInfo.path) { - R2.error("Bun is required to start the worker but was not found."); - R2.info("Install Bun: curl -fsSL https://bun.sh/install | bash"); - return; - } - const workerScript = join4(MARKETPLACE_DIR2, "plugin", "scripts", "worker-service.cjs"); - const expandedDataDir = expandHome(dataDir); - const logPath = join4(expandedDataDir, "logs"); - const s = bt2(); - s.start("Starting worker service..."); - const child = spawn(bunInfo.path, [workerScript], { - cwd: MARKETPLACE_DIR2, - detached: true, - stdio: "ignore", - env: { - ...process.env, - CLAUDE_MEM_WORKER_PORT: workerPort, - CLAUDE_MEM_DATA_DIR: expandedDataDir - } - }); - child.unref(); - const workerIsHealthy = await pollHealthEndpoint(workerPort); - if (workerIsHealthy) { - s.stop(`Worker running on port ${import_picocolors7.default.cyan(workerPort)} ${import_picocolors7.default.green("OK")}`); - } else { - s.stop(`Worker may still be starting. Check logs at: ${logPath}`); - R2.warn("Health check timed out. The worker might need more time to initialize."); - R2.info(`Check status: curl http://127.0.0.1:${workerPort}/api/health`); - } -} - -// src/steps/complete.ts -var import_picocolors8 = __toESM(require_picocolors(), 1); -function getProviderLabel(config) { - switch (config.provider) { - case "claude": - return config.claudeAuthMethod === "api" ? "Claude (API Key)" : "Claude (CLI subscription)"; - case "gemini": - return `Gemini (${config.model ?? "gemini-2.5-flash-lite"})`; - case "openrouter": - return `OpenRouter (${config.model ?? "xiaomi/mimo-v2-flash:free"})`; - } -} -function getIDELabels(ides) { - return ides.map((ide) => { - switch (ide) { - case "claude-code": - return "Claude Code"; - case "cursor": - return "Cursor"; - } - }).join(", "); -} -function runCompletion(providerConfig, settingsConfig, selectedIDEs) { - const summaryLines = [ - `Provider: ${import_picocolors8.default.cyan(getProviderLabel(providerConfig))}`, - `IDEs: ${import_picocolors8.default.cyan(getIDELabels(selectedIDEs))}`, - `Data dir: ${import_picocolors8.default.cyan(settingsConfig.dataDir)}`, - `Port: ${import_picocolors8.default.cyan(settingsConfig.workerPort)}`, - `Chroma: ${settingsConfig.chromaEnabled ? import_picocolors8.default.green("enabled") : import_picocolors8.default.dim("disabled")}` - ]; - Ve(summaryLines.join("\n"), "Configuration Summary"); - const nextStepsLines = []; - if (selectedIDEs.includes("claude-code")) { - nextStepsLines.push("Open Claude Code and start a conversation \u2014 memory is automatic!"); - } - if (selectedIDEs.includes("cursor")) { - nextStepsLines.push("Open Cursor \u2014 hooks are active in your projects."); - } - nextStepsLines.push(`View your memories: ${import_picocolors8.default.underline(`http://localhost:${settingsConfig.workerPort}`)}`); - nextStepsLines.push(`Search past work: use ${import_picocolors8.default.bold("/mem-search")} in Claude Code`); - Ve(nextStepsLines.join("\n"), "Next Steps"); - Le(import_picocolors8.default.green("claude-mem installed successfully!")); -} - -// src/index.ts -async function runInstaller() { - if (!process.stdin.isTTY) { - console.error("Error: This installer requires an interactive terminal."); - console.error("Run directly: npx claude-mem-installer"); - process.exit(1); - } - const installMode = await runWelcome(); - await runDependencyChecks(); - const selectedIDEs = await runIdeSelection(); - const providerConfig = await runProviderConfiguration(); - const settingsConfig = await runSettingsConfiguration(); - writeSettings(providerConfig, settingsConfig); - R2.success("Settings saved."); - if (installMode !== "configure") { - await runInstallation(selectedIDEs); - await runWorkerStartup(settingsConfig.workerPort, settingsConfig.dataDir); - } - runCompletion(providerConfig, settingsConfig, selectedIDEs); -} -runInstaller().catch((error) => { - Ne("Installation failed."); - console.error(error); - process.exit(1); -}); diff --git a/installer/package.json b/installer/package.json deleted file mode 100644 index 963c76a45..000000000 --- a/installer/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "claude-mem-installer", - "version": "1.0.0", - "type": "module", - "bin": { "claude-mem-installer": "./dist/index.js" }, - "files": ["dist"], - "scripts": { - "build": "node build.mjs", - "dev": "node build.mjs && node dist/index.js" - }, - "dependencies": { - "@clack/prompts": "^1.0.1", - "picocolors": "^1.1.1" - }, - "devDependencies": { - "esbuild": "^0.24.0", - "typescript": "^5.7.0", - "@types/node": "^22.0.0" - }, - "engines": { "node": ">=18.0.0" } -} diff --git a/installer/src/index.ts b/installer/src/index.ts deleted file mode 100644 index 509a5bacd..000000000 --- a/installer/src/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as p from '@clack/prompts'; -import { runWelcome } from './steps/welcome.js'; -import { runDependencyChecks } from './steps/dependencies.js'; -import { runIdeSelection } from './steps/ide-selection.js'; -import { runProviderConfiguration } from './steps/provider.js'; -import { runSettingsConfiguration } from './steps/settings.js'; -import { writeSettings } from './utils/settings-writer.js'; -import { runInstallation } from './steps/install.js'; -import { runWorkerStartup } from './steps/worker.js'; -import { runCompletion } from './steps/complete.js'; - -async function runInstaller(): Promise { - if (!process.stdin.isTTY) { - console.error('Error: This installer requires an interactive terminal.'); - console.error('Run directly: npx claude-mem-installer'); - process.exit(1); - } - - const installMode = await runWelcome(); - - // Dependency checks (all modes) - await runDependencyChecks(); - - // IDE and provider selection - const selectedIDEs = await runIdeSelection(); - const providerConfig = await runProviderConfiguration(); - - // Settings configuration - const settingsConfig = await runSettingsConfiguration(); - - // Write settings file - writeSettings(providerConfig, settingsConfig); - p.log.success('Settings saved.'); - - // Installation (fresh or upgrade) - if (installMode !== 'configure') { - await runInstallation(selectedIDEs); - await runWorkerStartup(settingsConfig.workerPort, settingsConfig.dataDir); - } - - // Completion summary - runCompletion(providerConfig, settingsConfig, selectedIDEs); -} - -runInstaller().catch((error) => { - p.cancel('Installation failed.'); - console.error(error); - process.exit(1); -}); diff --git a/installer/src/steps/complete.ts b/installer/src/steps/complete.ts deleted file mode 100644 index ca32bb8ca..000000000 --- a/installer/src/steps/complete.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as p from '@clack/prompts'; -import pc from 'picocolors'; -import type { ProviderConfig } from './provider.js'; -import type { SettingsConfig } from './settings.js'; -import type { IDE } from './ide-selection.js'; - -function getProviderLabel(config: ProviderConfig): string { - switch (config.provider) { - case 'claude': - return config.claudeAuthMethod === 'api' ? 'Claude (API Key)' : 'Claude (CLI subscription)'; - case 'gemini': - return `Gemini (${config.model ?? 'gemini-2.5-flash-lite'})`; - case 'openrouter': - return `OpenRouter (${config.model ?? 'xiaomi/mimo-v2-flash:free'})`; - } -} - -function getIDELabels(ides: IDE[]): string { - return ides.map((ide) => { - switch (ide) { - case 'claude-code': return 'Claude Code'; - case 'cursor': return 'Cursor'; - } - }).join(', '); -} - -export function runCompletion( - providerConfig: ProviderConfig, - settingsConfig: SettingsConfig, - selectedIDEs: IDE[], -): void { - const summaryLines = [ - `Provider: ${pc.cyan(getProviderLabel(providerConfig))}`, - `IDEs: ${pc.cyan(getIDELabels(selectedIDEs))}`, - `Data dir: ${pc.cyan(settingsConfig.dataDir)}`, - `Port: ${pc.cyan(settingsConfig.workerPort)}`, - `Chroma: ${settingsConfig.chromaEnabled ? pc.green('enabled') : pc.dim('disabled')}`, - ]; - - p.note(summaryLines.join('\n'), 'Configuration Summary'); - - const nextStepsLines: string[] = []; - - if (selectedIDEs.includes('claude-code')) { - nextStepsLines.push('Open Claude Code and start a conversation — memory is automatic!'); - } - if (selectedIDEs.includes('cursor')) { - nextStepsLines.push('Open Cursor — hooks are active in your projects.'); - } - nextStepsLines.push(`View your memories: ${pc.underline(`http://localhost:${settingsConfig.workerPort}`)}`); - nextStepsLines.push(`Search past work: use ${pc.bold('/mem-search')} in Claude Code`); - - p.note(nextStepsLines.join('\n'), 'Next Steps'); - - p.outro(pc.green('claude-mem installed successfully!')); -} diff --git a/installer/src/steps/dependencies.ts b/installer/src/steps/dependencies.ts deleted file mode 100644 index 242c7a992..000000000 --- a/installer/src/steps/dependencies.ts +++ /dev/null @@ -1,168 +0,0 @@ -import * as p from '@clack/prompts'; -import pc from 'picocolors'; -import { findBinary, compareVersions, installBun, installUv } from '../utils/dependencies.js'; -import { detectOS } from '../utils/system.js'; - -const BUN_EXTRA_PATHS = ['~/.bun/bin/bun', '/usr/local/bin/bun', '/opt/homebrew/bin/bun']; -const UV_EXTRA_PATHS = ['~/.local/bin/uv', '~/.cargo/bin/uv']; - -interface DependencyStatus { - nodeOk: boolean; - gitOk: boolean; - bunOk: boolean; - uvOk: boolean; - bunPath: string | null; - uvPath: string | null; -} - -export async function runDependencyChecks(): Promise { - const status: DependencyStatus = { - nodeOk: false, - gitOk: false, - bunOk: false, - uvOk: false, - bunPath: null, - uvPath: null, - }; - - await p.tasks([ - { - title: 'Checking Node.js', - task: async () => { - const version = process.version.slice(1); // remove 'v' - if (compareVersions(version, '18.0.0')) { - status.nodeOk = true; - return `Node.js ${process.version} ${pc.green('✓')}`; - } - return `Node.js ${process.version} — requires >= 18.0.0 ${pc.red('✗')}`; - }, - }, - { - title: 'Checking git', - task: async () => { - const info = findBinary('git'); - if (info.found) { - status.gitOk = true; - return `git ${info.version ?? ''} ${pc.green('✓')}`; - } - return `git not found ${pc.red('✗')}`; - }, - }, - { - title: 'Checking Bun', - task: async () => { - const info = findBinary('bun', BUN_EXTRA_PATHS); - if (info.found && info.version && compareVersions(info.version, '1.1.14')) { - status.bunOk = true; - status.bunPath = info.path; - return `Bun ${info.version} ${pc.green('✓')}`; - } - if (info.found && info.version) { - return `Bun ${info.version} — requires >= 1.1.14 ${pc.yellow('⚠')}`; - } - return `Bun not found ${pc.yellow('⚠')}`; - }, - }, - { - title: 'Checking uv', - task: async () => { - const info = findBinary('uv', UV_EXTRA_PATHS); - if (info.found) { - status.uvOk = true; - status.uvPath = info.path; - return `uv ${info.version ?? ''} ${pc.green('✓')}`; - } - return `uv not found ${pc.yellow('⚠')}`; - }, - }, - ]); - - // Handle missing dependencies - if (!status.gitOk) { - const os = detectOS(); - p.log.error('git is required but not found.'); - if (os === 'macos') { - p.log.info('Install with: xcode-select --install'); - } else if (os === 'linux') { - p.log.info('Install with: sudo apt install git (or your distro equivalent)'); - } else { - p.log.info('Download from: https://git-scm.com/downloads'); - } - p.cancel('Please install git and try again.'); - process.exit(1); - } - - if (!status.nodeOk) { - p.log.error(`Node.js >= 18.0.0 is required. Current: ${process.version}`); - p.cancel('Please upgrade Node.js and try again.'); - process.exit(1); - } - - if (!status.bunOk) { - const shouldInstall = await p.confirm({ - message: 'Bun is required but not found. Install it now?', - initialValue: true, - }); - - if (p.isCancel(shouldInstall)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - if (shouldInstall) { - const s = p.spinner(); - s.start('Installing Bun...'); - try { - installBun(); - const recheck = findBinary('bun', BUN_EXTRA_PATHS); - if (recheck.found) { - status.bunOk = true; - status.bunPath = recheck.path; - s.stop(`Bun installed ${pc.green('✓')}`); - } else { - s.stop(`Bun installed but not found in PATH. You may need to restart your shell.`); - } - } catch { - s.stop(`Bun installation failed. Install manually: curl -fsSL https://bun.sh/install | bash`); - } - } else { - p.log.warn('Bun is required for claude-mem. Install manually: curl -fsSL https://bun.sh/install | bash'); - p.cancel('Cannot continue without Bun.'); - process.exit(1); - } - } - - if (!status.uvOk) { - const shouldInstall = await p.confirm({ - message: 'uv (Python package manager) is recommended for Chroma. Install it now?', - initialValue: true, - }); - - if (p.isCancel(shouldInstall)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - if (shouldInstall) { - const s = p.spinner(); - s.start('Installing uv...'); - try { - installUv(); - const recheck = findBinary('uv', UV_EXTRA_PATHS); - if (recheck.found) { - status.uvOk = true; - status.uvPath = recheck.path; - s.stop(`uv installed ${pc.green('✓')}`); - } else { - s.stop('uv installed but not found in PATH. You may need to restart your shell.'); - } - } catch { - s.stop('uv installation failed. Install manually: curl -fsSL https://astral.sh/uv/install.sh | sh'); - } - } else { - p.log.warn('Skipping uv — Chroma vector search will not be available.'); - } - } - - return status; -} diff --git a/installer/src/steps/ide-selection.ts b/installer/src/steps/ide-selection.ts deleted file mode 100644 index eefad6dd3..000000000 --- a/installer/src/steps/ide-selection.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as p from '@clack/prompts'; - -export type IDE = 'claude-code' | 'cursor'; - -export async function runIdeSelection(): Promise { - const result = await p.multiselect({ - message: 'Which IDEs do you use?', - options: [ - { value: 'claude-code' as const, label: 'Claude Code', hint: 'recommended' }, - { value: 'cursor' as const, label: 'Cursor' }, - // Windsurf coming soon - not yet selectable - ], - initialValues: ['claude-code'], - required: true, - }); - - if (p.isCancel(result)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - const selectedIDEs = result as IDE[]; - - if (selectedIDEs.includes('claude-code')) { - p.log.info('Claude Code: Plugin will be registered via marketplace.'); - } - if (selectedIDEs.includes('cursor')) { - p.log.info('Cursor: Hooks will be configured for your projects.'); - } - - return selectedIDEs; -} diff --git a/installer/src/steps/install.ts b/installer/src/steps/install.ts deleted file mode 100644 index 88311f8b0..000000000 --- a/installer/src/steps/install.ts +++ /dev/null @@ -1,167 +0,0 @@ -import * as p from '@clack/prompts'; -import pc from 'picocolors'; -import { execSync } from 'child_process'; -import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from 'fs'; -import { join } from 'path'; -import { homedir, tmpdir } from 'os'; -import type { IDE } from './ide-selection.js'; - -const MARKETPLACE_DIR = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); -const PLUGINS_DIR = join(homedir(), '.claude', 'plugins'); -const CLAUDE_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json'); - -function ensureDir(directoryPath: string): void { - if (!existsSync(directoryPath)) { - mkdirSync(directoryPath, { recursive: true }); - } -} - -function readJsonFile(filepath: string): any { - if (!existsSync(filepath)) return {}; - return JSON.parse(readFileSync(filepath, 'utf-8')); -} - -function writeJsonFile(filepath: string, data: any): void { - ensureDir(join(filepath, '..')); - writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); -} - -function registerMarketplace(): void { - const knownMarketplacesPath = join(PLUGINS_DIR, 'known_marketplaces.json'); - const knownMarketplaces = readJsonFile(knownMarketplacesPath); - - knownMarketplaces['thedotmack'] = { - source: { - source: 'github', - repo: 'thedotmack/claude-mem', - }, - installLocation: MARKETPLACE_DIR, - lastUpdated: new Date().toISOString(), - autoUpdate: true, - }; - - ensureDir(PLUGINS_DIR); - writeJsonFile(knownMarketplacesPath, knownMarketplaces); -} - -function registerPlugin(version: string): void { - const installedPluginsPath = join(PLUGINS_DIR, 'installed_plugins.json'); - const installedPlugins = readJsonFile(installedPluginsPath); - - if (!installedPlugins.version) installedPlugins.version = 2; - if (!installedPlugins.plugins) installedPlugins.plugins = {}; - - const pluginCachePath = join(PLUGINS_DIR, 'cache', 'thedotmack', 'claude-mem', version); - const now = new Date().toISOString(); - - installedPlugins.plugins['claude-mem@thedotmack'] = [ - { - scope: 'user', - installPath: pluginCachePath, - version, - installedAt: now, - lastUpdated: now, - }, - ]; - - writeJsonFile(installedPluginsPath, installedPlugins); - - // Copy built plugin to cache directory - ensureDir(pluginCachePath); - const pluginSourceDir = join(MARKETPLACE_DIR, 'plugin'); - if (existsSync(pluginSourceDir)) { - cpSync(pluginSourceDir, pluginCachePath, { recursive: true }); - } -} - -function enablePluginInClaudeSettings(): void { - const settings = readJsonFile(CLAUDE_SETTINGS_PATH); - - if (!settings.enabledPlugins) settings.enabledPlugins = {}; - settings.enabledPlugins['claude-mem@thedotmack'] = true; - - writeJsonFile(CLAUDE_SETTINGS_PATH, settings); -} - -function getPluginVersion(): string { - const pluginJsonPath = join(MARKETPLACE_DIR, 'plugin', '.claude-plugin', 'plugin.json'); - if (existsSync(pluginJsonPath)) { - const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8')); - return pluginJson.version ?? '1.0.0'; - } - return '1.0.0'; -} - -export async function runInstallation(selectedIDEs: IDE[]): Promise { - const tempDir = join(tmpdir(), `claude-mem-install-${Date.now()}`); - - await p.tasks([ - { - title: 'Cloning claude-mem repository', - task: async (message) => { - message('Downloading latest release...'); - execSync( - `git clone --depth 1 https://github.com/thedotmack/claude-mem.git "${tempDir}"`, - { stdio: 'pipe' }, - ); - return `Repository cloned ${pc.green('OK')}`; - }, - }, - { - title: 'Installing dependencies', - task: async (message) => { - message('Running npm install...'); - execSync('npm install', { cwd: tempDir, stdio: 'pipe' }); - return `Dependencies installed ${pc.green('OK')}`; - }, - }, - { - title: 'Building plugin', - task: async (message) => { - message('Compiling TypeScript and bundling...'); - execSync('npm run build', { cwd: tempDir, stdio: 'pipe' }); - return `Plugin built ${pc.green('OK')}`; - }, - }, - { - title: 'Registering plugin', - task: async (message) => { - message('Copying files to marketplace directory...'); - ensureDir(MARKETPLACE_DIR); - - // Sync from cloned repo to marketplace dir, excluding .git and lock files - execSync( - `rsync -a --delete --exclude=.git --exclude=package-lock.json --exclude=bun.lock "${tempDir}/" "${MARKETPLACE_DIR}/"`, - { stdio: 'pipe' }, - ); - - message('Registering marketplace...'); - registerMarketplace(); - - message('Installing marketplace dependencies...'); - execSync('npm install', { cwd: MARKETPLACE_DIR, stdio: 'pipe' }); - - message('Registering plugin in Claude Code...'); - const version = getPluginVersion(); - registerPlugin(version); - - message('Enabling plugin...'); - enablePluginInClaudeSettings(); - - return `Plugin registered (v${getPluginVersion()}) ${pc.green('OK')}`; - }, - }, - ]); - - // Cleanup temp directory (non-critical if it fails) - try { - execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' }); - } catch { - // Temp dir will be cleaned by OS eventually - } - - if (selectedIDEs.includes('cursor')) { - p.log.info('Cursor hook configuration will be available after first launch.'); - p.log.info('Run: claude-mem cursor-setup (coming soon)'); - } -} diff --git a/installer/src/steps/provider.ts b/installer/src/steps/provider.ts deleted file mode 100644 index 6fcb6bfaa..000000000 --- a/installer/src/steps/provider.ts +++ /dev/null @@ -1,140 +0,0 @@ -import * as p from '@clack/prompts'; -import pc from 'picocolors'; - -export type ProviderType = 'claude' | 'gemini' | 'openrouter'; -export type ClaudeAuthMethod = 'cli' | 'api'; - -export interface ProviderConfig { - provider: ProviderType; - claudeAuthMethod?: ClaudeAuthMethod; - apiKey?: string; - model?: string; - rateLimitingEnabled?: boolean; -} - -export async function runProviderConfiguration(): Promise { - const provider = await p.select({ - message: 'Which AI provider should claude-mem use for memory compression?', - options: [ - { value: 'claude' as const, label: 'Claude', hint: 'uses your Claude subscription' }, - { value: 'gemini' as const, label: 'Gemini', hint: 'free tier available' }, - { value: 'openrouter' as const, label: 'OpenRouter', hint: 'free models available' }, - ], - }); - - if (p.isCancel(provider)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - const config: ProviderConfig = { provider }; - - if (provider === 'claude') { - const authMethod = await p.select({ - message: 'How should Claude authenticate?', - options: [ - { value: 'cli' as const, label: 'CLI (Max Plan subscription)', hint: 'no API key needed' }, - { value: 'api' as const, label: 'API Key', hint: 'uses Anthropic API credits' }, - ], - }); - - if (p.isCancel(authMethod)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - config.claudeAuthMethod = authMethod; - - if (authMethod === 'api') { - const apiKey = await p.password({ - message: 'Enter your Anthropic API key:', - validate: (value) => { - if (!value || value.trim().length === 0) return 'API key is required'; - if (!value.startsWith('sk-ant-')) return 'Anthropic API keys start with sk-ant-'; - }, - }); - - if (p.isCancel(apiKey)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - config.apiKey = apiKey; - } - } - - if (provider === 'gemini') { - const apiKey = await p.password({ - message: 'Enter your Gemini API key:', - validate: (value) => { - if (!value || value.trim().length === 0) return 'API key is required'; - }, - }); - - if (p.isCancel(apiKey)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - config.apiKey = apiKey; - - const model = await p.select({ - message: 'Which Gemini model?', - options: [ - { value: 'gemini-2.5-flash-lite' as const, label: 'Gemini 2.5 Flash Lite', hint: 'fastest, highest free RPM' }, - { value: 'gemini-2.5-flash' as const, label: 'Gemini 2.5 Flash', hint: 'balanced' }, - { value: 'gemini-3-flash-preview' as const, label: 'Gemini 3 Flash Preview', hint: 'latest' }, - ], - }); - - if (p.isCancel(model)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - config.model = model; - - const rateLimiting = await p.confirm({ - message: 'Enable rate limiting? (recommended for free tier)', - initialValue: true, - }); - - if (p.isCancel(rateLimiting)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - config.rateLimitingEnabled = rateLimiting; - } - - if (provider === 'openrouter') { - const apiKey = await p.password({ - message: 'Enter your OpenRouter API key:', - validate: (value) => { - if (!value || value.trim().length === 0) return 'API key is required'; - }, - }); - - if (p.isCancel(apiKey)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - config.apiKey = apiKey; - - const model = await p.text({ - message: 'Which OpenRouter model?', - defaultValue: 'xiaomi/mimo-v2-flash:free', - placeholder: 'xiaomi/mimo-v2-flash:free', - }); - - if (p.isCancel(model)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - config.model = model; - } - - return config; -} diff --git a/installer/src/steps/settings.ts b/installer/src/steps/settings.ts deleted file mode 100644 index 547da9275..000000000 --- a/installer/src/steps/settings.ts +++ /dev/null @@ -1,174 +0,0 @@ -import * as p from '@clack/prompts'; -import pc from 'picocolors'; - -export interface SettingsConfig { - workerPort: string; - dataDir: string; - contextObservations: string; - logLevel: string; - pythonVersion: string; - chromaEnabled: boolean; - chromaMode?: 'local' | 'remote'; - chromaHost?: string; - chromaPort?: string; - chromaSsl?: boolean; -} - -export async function runSettingsConfiguration(): Promise { - const useDefaults = await p.confirm({ - message: 'Use default settings? (recommended for most users)', - initialValue: true, - }); - - if (p.isCancel(useDefaults)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - if (useDefaults) { - return { - workerPort: '37777', - dataDir: '~/.claude-mem', - contextObservations: '50', - logLevel: 'INFO', - pythonVersion: '3.13', - chromaEnabled: true, - chromaMode: 'local', - }; - } - - // Custom settings - const workerPort = await p.text({ - message: 'Worker service port:', - defaultValue: '37777', - placeholder: '37777', - validate: (value = '') => { - const port = parseInt(value, 10); - if (isNaN(port) || port < 1024 || port > 65535) { - return 'Port must be between 1024 and 65535'; - } - }, - }); - if (p.isCancel(workerPort)) { p.cancel('Installation cancelled.'); process.exit(0); } - - const dataDir = await p.text({ - message: 'Data directory:', - defaultValue: '~/.claude-mem', - placeholder: '~/.claude-mem', - }); - if (p.isCancel(dataDir)) { p.cancel('Installation cancelled.'); process.exit(0); } - - const contextObservations = await p.text({ - message: 'Number of context observations per session:', - defaultValue: '50', - placeholder: '50', - validate: (value = '') => { - const num = parseInt(value, 10); - if (isNaN(num) || num < 1 || num > 200) { - return 'Must be between 1 and 200'; - } - }, - }); - if (p.isCancel(contextObservations)) { p.cancel('Installation cancelled.'); process.exit(0); } - - const logLevel = await p.select({ - message: 'Log level:', - options: [ - { value: 'DEBUG', label: 'DEBUG', hint: 'verbose' }, - { value: 'INFO', label: 'INFO', hint: 'default' }, - { value: 'WARN', label: 'WARN' }, - { value: 'ERROR', label: 'ERROR', hint: 'errors only' }, - ], - initialValue: 'INFO', - }); - if (p.isCancel(logLevel)) { p.cancel('Installation cancelled.'); process.exit(0); } - - const pythonVersion = await p.text({ - message: 'Python version (for Chroma):', - defaultValue: '3.13', - placeholder: '3.13', - }); - if (p.isCancel(pythonVersion)) { p.cancel('Installation cancelled.'); process.exit(0); } - - const chromaEnabled = await p.confirm({ - message: 'Enable Chroma vector search?', - initialValue: true, - }); - if (p.isCancel(chromaEnabled)) { p.cancel('Installation cancelled.'); process.exit(0); } - - let chromaMode: 'local' | 'remote' | undefined; - let chromaHost: string | undefined; - let chromaPort: string | undefined; - let chromaSsl: boolean | undefined; - - if (chromaEnabled) { - const mode = await p.select({ - message: 'Chroma mode:', - options: [ - { value: 'local' as const, label: 'Local', hint: 'starts local Chroma server' }, - { value: 'remote' as const, label: 'Remote', hint: 'connect to existing server' }, - ], - }); - if (p.isCancel(mode)) { p.cancel('Installation cancelled.'); process.exit(0); } - chromaMode = mode; - - if (mode === 'remote') { - const host = await p.text({ - message: 'Chroma host:', - defaultValue: '127.0.0.1', - placeholder: '127.0.0.1', - }); - if (p.isCancel(host)) { p.cancel('Installation cancelled.'); process.exit(0); } - chromaHost = host; - - const port = await p.text({ - message: 'Chroma port:', - defaultValue: '8000', - placeholder: '8000', - validate: (value = '') => { - const portNum = parseInt(value, 10); - if (isNaN(portNum) || portNum < 1 || portNum > 65535) return 'Port must be between 1 and 65535'; - }, - }); - if (p.isCancel(port)) { p.cancel('Installation cancelled.'); process.exit(0); } - chromaPort = port; - - const ssl = await p.confirm({ - message: 'Use SSL for Chroma connection?', - initialValue: false, - }); - if (p.isCancel(ssl)) { p.cancel('Installation cancelled.'); process.exit(0); } - chromaSsl = ssl; - } - } - - const config: SettingsConfig = { - workerPort, - dataDir, - contextObservations, - logLevel, - pythonVersion, - chromaEnabled, - chromaMode, - chromaHost, - chromaPort, - chromaSsl, - }; - - // Show summary - const summaryLines = [ - `Worker port: ${pc.cyan(workerPort)}`, - `Data directory: ${pc.cyan(dataDir)}`, - `Context observations: ${pc.cyan(contextObservations)}`, - `Log level: ${pc.cyan(logLevel)}`, - `Python version: ${pc.cyan(pythonVersion)}`, - `Chroma: ${chromaEnabled ? pc.green('enabled') : pc.dim('disabled')}`, - ]; - if (chromaEnabled && chromaMode) { - summaryLines.push(`Chroma mode: ${pc.cyan(chromaMode)}`); - } - - p.note(summaryLines.join('\n'), 'Settings Summary'); - - return config; -} diff --git a/installer/src/steps/welcome.ts b/installer/src/steps/welcome.ts deleted file mode 100644 index 221a51bb0..000000000 --- a/installer/src/steps/welcome.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as p from '@clack/prompts'; -import pc from 'picocolors'; -import { existsSync } from 'fs'; -import { expandHome } from '../utils/system.js'; - -export type InstallMode = 'fresh' | 'upgrade' | 'configure'; - -export async function runWelcome(): Promise { - p.intro(pc.bgCyan(pc.black(' claude-mem installer '))); - - p.log.info(`Version: 1.0.0`); - p.log.info(`Platform: ${process.platform} (${process.arch})`); - - const settingsExist = existsSync(expandHome('~/.claude-mem/settings.json')); - const pluginExist = existsSync(expandHome('~/.claude/plugins/marketplaces/thedotmack/')); - - const alreadyInstalled = settingsExist && pluginExist; - - if (alreadyInstalled) { - p.log.warn('Existing claude-mem installation detected.'); - } - - const installMode = await p.select({ - message: 'What would you like to do?', - options: alreadyInstalled - ? [ - { value: 'upgrade' as const, label: 'Upgrade', hint: 'update to latest version' }, - { value: 'configure' as const, label: 'Configure', hint: 'change settings only' }, - { value: 'fresh' as const, label: 'Fresh Install', hint: 'reinstall from scratch' }, - ] - : [ - { value: 'fresh' as const, label: 'Fresh Install', hint: 'recommended' }, - { value: 'configure' as const, label: 'Configure Only', hint: 'set up settings without installing' }, - ], - }); - - if (p.isCancel(installMode)) { - p.cancel('Installation cancelled.'); - process.exit(0); - } - - return installMode; -} diff --git a/installer/src/steps/worker.ts b/installer/src/steps/worker.ts deleted file mode 100644 index 595cb6789..000000000 --- a/installer/src/steps/worker.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as p from '@clack/prompts'; -import pc from 'picocolors'; -import { spawn } from 'child_process'; -import { join } from 'path'; -import { homedir } from 'os'; -import { expandHome } from '../utils/system.js'; -import { findBinary } from '../utils/dependencies.js'; - -const MARKETPLACE_DIR = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); - -const HEALTH_CHECK_INTERVAL_MS = 1000; -const HEALTH_CHECK_MAX_ATTEMPTS = 30; - -async function pollHealthEndpoint(port: string, maxAttempts: number = HEALTH_CHECK_MAX_ATTEMPTS): Promise { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - try { - const response = await fetch(`http://127.0.0.1:${port}/api/health`); - if (response.ok) return true; - } catch { - // Expected during startup — worker not listening yet - } - await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS)); - } - return false; -} - -export async function runWorkerStartup(workerPort: string, dataDir: string): Promise { - const bunInfo = findBinary('bun', ['~/.bun/bin/bun', '/usr/local/bin/bun', '/opt/homebrew/bin/bun']); - - if (!bunInfo.found || !bunInfo.path) { - p.log.error('Bun is required to start the worker but was not found.'); - p.log.info('Install Bun: curl -fsSL https://bun.sh/install | bash'); - return; - } - - const workerScript = join(MARKETPLACE_DIR, 'plugin', 'scripts', 'worker-service.cjs'); - const expandedDataDir = expandHome(dataDir); - const logPath = join(expandedDataDir, 'logs'); - - const s = p.spinner(); - s.start('Starting worker service...'); - - // Start worker as a detached background process - const child = spawn(bunInfo.path, [workerScript], { - cwd: MARKETPLACE_DIR, - detached: true, - stdio: 'ignore', - env: { - ...process.env, - CLAUDE_MEM_WORKER_PORT: workerPort, - CLAUDE_MEM_DATA_DIR: expandedDataDir, - }, - }); - - child.unref(); - - // Poll the health endpoint until the worker is responsive - const workerIsHealthy = await pollHealthEndpoint(workerPort); - - if (workerIsHealthy) { - s.stop(`Worker running on port ${pc.cyan(workerPort)} ${pc.green('OK')}`); - } else { - s.stop(`Worker may still be starting. Check logs at: ${logPath}`); - p.log.warn('Health check timed out. The worker might need more time to initialize.'); - p.log.info(`Check status: curl http://127.0.0.1:${workerPort}/api/health`); - } -} diff --git a/installer/src/utils/dependencies.ts b/installer/src/utils/dependencies.ts deleted file mode 100644 index 74d1af9c4..000000000 --- a/installer/src/utils/dependencies.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { existsSync } from 'fs'; -import { execSync } from 'child_process'; -import { commandExists, runCommand, expandHome, detectOS } from './system.js'; - -export interface BinaryInfo { - found: boolean; - path: string | null; - version: string | null; -} - -export function findBinary(name: string, extraPaths: string[] = []): BinaryInfo { - // Check PATH first - if (commandExists(name)) { - const result = runCommand('which', [name]); - const versionResult = runCommand(name, ['--version']); - return { - found: true, - path: result.stdout, - version: parseVersion(versionResult.stdout) || parseVersion(versionResult.stderr), - }; - } - - // Check extra known locations - for (const extraPath of extraPaths) { - const fullPath = expandHome(extraPath); - if (existsSync(fullPath)) { - const versionResult = runCommand(fullPath, ['--version']); - return { - found: true, - path: fullPath, - version: parseVersion(versionResult.stdout) || parseVersion(versionResult.stderr), - }; - } - } - - return { found: false, path: null, version: null }; -} - -function parseVersion(output: string): string | null { - if (!output) return null; - const match = output.match(/(\d+\.\d+(\.\d+)?)/); - return match ? match[1] : null; -} - -export function compareVersions(current: string, minimum: string): boolean { - const currentParts = current.split('.').map(Number); - const minimumParts = minimum.split('.').map(Number); - - for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i++) { - const a = currentParts[i] || 0; - const b = minimumParts[i] || 0; - if (a > b) return true; - if (a < b) return false; - } - return true; // equal -} - -export function installBun(): void { - const os = detectOS(); - if (os === 'windows') { - execSync('powershell -c "irm bun.sh/install.ps1 | iex"', { stdio: 'inherit' }); - } else { - execSync('curl -fsSL https://bun.sh/install | bash', { stdio: 'inherit' }); - } -} - -export function installUv(): void { - const os = detectOS(); - if (os === 'windows') { - execSync('powershell -c "irm https://astral.sh/uv/install.ps1 | iex"', { stdio: 'inherit' }); - } else { - execSync('curl -fsSL https://astral.sh/uv/install.sh | sh', { stdio: 'inherit' }); - } -} diff --git a/installer/src/utils/settings-writer.ts b/installer/src/utils/settings-writer.ts deleted file mode 100644 index 8aca67d4d..000000000 --- a/installer/src/utils/settings-writer.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; -import { join } from 'path'; -import { homedir } from 'os'; -import type { ProviderConfig } from '../steps/provider.js'; -import type { SettingsConfig } from '../steps/settings.js'; - -export function expandDataDir(dataDir: string): string { - if (dataDir.startsWith('~')) { - return join(homedir(), dataDir.slice(1)); - } - return dataDir; -} - -export function buildSettingsObject( - providerConfig: ProviderConfig, - settingsConfig: SettingsConfig, -): Record { - const settings: Record = { - CLAUDE_MEM_WORKER_PORT: settingsConfig.workerPort, - CLAUDE_MEM_WORKER_HOST: '127.0.0.1', - CLAUDE_MEM_DATA_DIR: expandDataDir(settingsConfig.dataDir), - CLAUDE_MEM_CONTEXT_OBSERVATIONS: settingsConfig.contextObservations, - CLAUDE_MEM_LOG_LEVEL: settingsConfig.logLevel, - CLAUDE_MEM_PYTHON_VERSION: settingsConfig.pythonVersion, - CLAUDE_MEM_PROVIDER: providerConfig.provider, - }; - - // Provider-specific settings - if (providerConfig.provider === 'claude') { - settings.CLAUDE_MEM_CLAUDE_AUTH_METHOD = providerConfig.claudeAuthMethod ?? 'cli'; - } - - if (providerConfig.provider === 'gemini') { - if (providerConfig.apiKey) settings.CLAUDE_MEM_GEMINI_API_KEY = providerConfig.apiKey; - if (providerConfig.model) settings.CLAUDE_MEM_GEMINI_MODEL = providerConfig.model; - settings.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED = providerConfig.rateLimitingEnabled !== false ? 'true' : 'false'; - } - - if (providerConfig.provider === 'openrouter') { - if (providerConfig.apiKey) settings.CLAUDE_MEM_OPENROUTER_API_KEY = providerConfig.apiKey; - if (providerConfig.model) settings.CLAUDE_MEM_OPENROUTER_MODEL = providerConfig.model; - } - - // Chroma settings - if (settingsConfig.chromaEnabled) { - settings.CLAUDE_MEM_CHROMA_MODE = settingsConfig.chromaMode ?? 'local'; - if (settingsConfig.chromaMode === 'remote') { - if (settingsConfig.chromaHost) settings.CLAUDE_MEM_CHROMA_HOST = settingsConfig.chromaHost; - if (settingsConfig.chromaPort) settings.CLAUDE_MEM_CHROMA_PORT = settingsConfig.chromaPort; - if (settingsConfig.chromaSsl !== undefined) settings.CLAUDE_MEM_CHROMA_SSL = String(settingsConfig.chromaSsl); - } - } - - return settings; -} - -export function writeSettings( - providerConfig: ProviderConfig, - settingsConfig: SettingsConfig, -): void { - const dataDir = expandDataDir(settingsConfig.dataDir); - const settingsPath = join(dataDir, 'settings.json'); - - // Ensure data directory exists - if (!existsSync(dataDir)) { - mkdirSync(dataDir, { recursive: true }); - } - - // Merge with existing settings if upgrading - let existingSettings: Record = {}; - if (existsSync(settingsPath)) { - const raw = readFileSync(settingsPath, 'utf-8'); - existingSettings = JSON.parse(raw); - } - - const newSettings = buildSettingsObject(providerConfig, settingsConfig); - - // Merge: new settings override existing ones - const merged = { ...existingSettings, ...newSettings }; - - writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8'); -} diff --git a/installer/src/utils/system.ts b/installer/src/utils/system.ts deleted file mode 100644 index 2c5ddcfcb..000000000 --- a/installer/src/utils/system.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { execSync } from 'child_process'; -import { homedir } from 'os'; -import { join } from 'path'; - -export type OSType = 'macos' | 'linux' | 'windows'; - -export function detectOS(): OSType { - switch (process.platform) { - case 'darwin': return 'macos'; - case 'win32': return 'windows'; - default: return 'linux'; - } -} - -export function commandExists(command: string): boolean { - try { - execSync(`which ${command}`, { stdio: 'pipe' }); - return true; - } catch { - return false; - } -} - -export interface CommandResult { - stdout: string; - stderr: string; - exitCode: number; -} - -export function runCommand(command: string, args: string[] = []): CommandResult { - try { - const fullCommand = [command, ...args].join(' '); - const stdout = execSync(fullCommand, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); - return { stdout: stdout.trim(), stderr: '', exitCode: 0 }; - } catch (error: any) { - return { - stdout: error.stdout?.toString().trim() ?? '', - stderr: error.stderr?.toString().trim() ?? '', - exitCode: error.status ?? 1, - }; - } -} - -export function expandHome(filepath: string): string { - if (filepath.startsWith('~')) { - return join(homedir(), filepath.slice(1)); - } - return filepath; -} diff --git a/installer/tsconfig.json b/installer/tsconfig.json deleted file mode 100644 index 69662ae07..000000000 --- a/installer/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "ESNext", - "target": "ES2022", - "moduleResolution": "bundler", - "esModuleInterop": true, - "strict": true, - "outDir": "dist", - "rootDir": "src", - "declaration": false, - "skipLibCheck": true, - "resolveJsonModule": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/openclaw/.npmignore b/openclaw/.npmignore new file mode 100644 index 000000000..c2658d7d1 --- /dev/null +++ b/openclaw/.npmignore @@ -0,0 +1 @@ +node_modules/ diff --git a/package.json b/package.json index fc64334fe..489ec1ae8 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "url": "https://github.com/thedotmack/claude-mem/issues" }, "type": "module", + "bin": { + "claude-mem": "./dist/npx-cli/index.js" + }, "exports": { ".": { "types": "./dist/index.d.ts", @@ -39,7 +42,17 @@ }, "files": [ "dist", - "plugin" + "plugin/.claude-plugin", + "plugin/CLAUDE.md", + "plugin/package.json", + "plugin/hooks", + "plugin/modes", + "plugin/scripts/*.js", + "plugin/scripts/*.cjs", + "plugin/scripts/CLAUDE.md", + "plugin/skills", + "plugin/ui", + "openclaw" ], "engines": { "node": ">=18.0.0", @@ -97,12 +110,14 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.76", + "@clack/prompts": "^0.9.1", "@modelcontextprotocol/sdk": "^1.25.1", "ansi-to-html": "^0.7.2", "dompurify": "^3.3.1", "express": "^4.18.2", "glob": "^11.0.3", "handlebars": "^4.7.8", + "picocolors": "^1.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "yaml": "^2.8.2", diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index 519d196e9..9d978355e 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -182,6 +182,88 @@ async function buildHooks() { const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`); console.log(`✓ context-generator built (${(contextGenStats.size / 1024).toFixed(2)} KB)`); + // Build NPX CLI (pure Node.js — no Bun dependency) + console.log(`\n🔧 Building NPX CLI...`); + const npxCliOutDir = 'dist/npx-cli'; + if (!fs.existsSync(npxCliOutDir)) { + fs.mkdirSync(npxCliOutDir, { recursive: true }); + } + await build({ + entryPoints: ['src/npx-cli/index.ts'], + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outfile: `${npxCliOutDir}/index.js`, + minify: true, + logLevel: 'error', + external: [ + 'fs', 'fs/promises', 'path', 'os', 'child_process', 'url', + 'crypto', 'http', 'https', 'net', 'stream', 'util', 'events', + 'buffer', 'querystring', 'readline', 'tty', 'assert', + ], + define: { + '__DEFAULT_PACKAGE_VERSION__': `"${version}"` + }, + }); + + // Make NPX CLI executable + fs.chmodSync(`${npxCliOutDir}/index.js`, 0o755); + const npxCliStats = fs.statSync(`${npxCliOutDir}/index.js`); + console.log(`✓ npx-cli built (${(npxCliStats.size / 1024).toFixed(2)} KB)`); + + // Build OpenClaw plugin (self-contained, only Node builtins external) + if (fs.existsSync('openclaw/src/index.ts')) { + console.log(`\n🔧 Building OpenClaw plugin...`); + const openclawOutDir = 'openclaw/dist'; + if (!fs.existsSync(openclawOutDir)) { + fs.mkdirSync(openclawOutDir, { recursive: true }); + } + await build({ + entryPoints: ['openclaw/src/index.ts'], + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outfile: `${openclawOutDir}/index.js`, + minify: true, + logLevel: 'error', + external: [ + 'fs', 'fs/promises', 'path', 'os', 'child_process', 'url', + 'crypto', 'http', 'https', 'net', 'stream', 'util', 'events', + ], + }); + + const openclawStats = fs.statSync(`${openclawOutDir}/index.js`); + console.log(`✓ openclaw plugin built (${(openclawStats.size / 1024).toFixed(2)} KB)`); + } + + // Build OpenCode plugin (self-contained, runs in Bun) + if (fs.existsSync('src/integrations/opencode-plugin/index.ts')) { + console.log(`\n🔧 Building OpenCode plugin...`); + const opencodeOutDir = 'dist/opencode-plugin'; + if (!fs.existsSync(opencodeOutDir)) { + fs.mkdirSync(opencodeOutDir, { recursive: true }); + } + await build({ + entryPoints: ['src/integrations/opencode-plugin/index.ts'], + bundle: true, + platform: 'node', + target: 'node18', + format: 'esm', + outfile: `${opencodeOutDir}/index.js`, + minify: true, + logLevel: 'error', + external: [ + 'fs', 'fs/promises', 'path', 'os', 'child_process', 'url', + 'crypto', 'http', 'https', 'net', 'stream', 'util', 'events', + ], + }); + + const opencodeStats = fs.statSync(`${opencodeOutDir}/index.js`); + console.log(`✓ opencode plugin built (${(opencodeStats.size / 1024).toFixed(2)} KB)`); + } + // Verify critical distribution files exist (skills are source files, not build outputs) console.log('\n📋 Verifying distribution files...'); const requiredDistributionFiles = [ @@ -197,11 +279,21 @@ async function buildHooks() { } console.log('✓ All required distribution files present'); - console.log('\n✅ Worker service, MCP server, and context generator built successfully!'); + console.log('\n✅ All build targets compiled successfully!'); console.log(` Output: ${hooksDir}/`); console.log(` - Worker: worker-service.cjs`); console.log(` - MCP Server: mcp-server.cjs`); console.log(` - Context Generator: context-generator.cjs`); + console.log(` Output: ${npxCliOutDir}/`); + console.log(` - NPX CLI: index.js`); + if (fs.existsSync('openclaw/dist/index.js')) { + console.log(` Output: openclaw/dist/`); + console.log(` - OpenClaw Plugin: index.js`); + } + if (fs.existsSync('dist/opencode-plugin/index.js')) { + console.log(` Output: dist/opencode-plugin/`); + console.log(` - OpenCode Plugin: index.js`); + } } catch (error) { console.error('\n❌ Build failed:', error.message); diff --git a/src/cli/adapters/gemini-cli.ts b/src/cli/adapters/gemini-cli.ts new file mode 100644 index 000000000..db03daed7 --- /dev/null +++ b/src/cli/adapters/gemini-cli.ts @@ -0,0 +1,85 @@ +import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.js'; + +/** + * Gemini CLI Platform Adapter + * + * Normalizes Gemini CLI's hook JSON to NormalizedHookInput. + * Gemini CLI has 11 lifecycle hooks; we map 6 of them: + * SessionStart → session-init + * BeforeAgent → user-message (captures prompt) + * AfterAgent → observation (full response) + * AfterTool → observation (tool result) + * PreCompress → summarize + * SessionEnd → session-complete + * + * Base fields (all events): session_id, transcript_path, cwd, hook_event_name, timestamp + * + * Output format: { continue, stopReason, suppressOutput, systemMessage, decision, reason } + * Advisory hooks (SessionStart, SessionEnd, PreCompress) ignore `continue` and `decision`. + */ +export const geminiCliAdapter: PlatformAdapter = { + normalizeInput(raw) { + const r = (raw ?? {}) as any; + + // Use GEMINI_CWD, GEMINI_PROJECT_DIR, or the JSON cwd field + const cwd = r.cwd + ?? process.env.GEMINI_CWD + ?? process.env.GEMINI_PROJECT_DIR + ?? process.env.CLAUDE_PROJECT_DIR + ?? process.cwd(); + + const sessionId = r.session_id + ?? process.env.GEMINI_SESSION_ID + ?? undefined; + + // Map event-specific fields into normalized shape + // AfterTool provides tool_name, tool_input, tool_response + // BeforeAgent/AfterAgent provide prompt (and prompt_response for AfterAgent) + const hookEventName: string | undefined = r.hook_event_name; + + // For AfterAgent, treat the full response as an observation by packing it + // into toolResponse so the observation handler can process it + let toolName: string | undefined = r.tool_name; + let toolInput: unknown = r.tool_input; + let toolResponse: unknown = r.tool_response; + + if (hookEventName === 'AfterAgent' && r.prompt_response) { + toolName = toolName ?? 'GeminiAgent'; + toolInput = toolInput ?? { prompt: r.prompt }; + toolResponse = toolResponse ?? { response: r.prompt_response }; + } + + return { + sessionId, + cwd, + prompt: r.prompt, + toolName, + toolInput, + toolResponse, + transcriptPath: r.transcript_path, + }; + }, + + formatOutput(result) { + // Gemini CLI expects: { continue, stopReason, suppressOutput, systemMessage, decision, reason } + const output: Record = {}; + + // Always include continue — controls whether the agent proceeds + output.continue = result.continue ?? true; + + if (result.suppressOutput !== undefined) { + output.suppressOutput = result.suppressOutput; + } + + if (result.systemMessage) { + output.systemMessage = result.systemMessage; + } + + // hookSpecificOutput carries context injection data + if (result.hookSpecificOutput) { + output.systemMessage = result.hookSpecificOutput.additionalContext || output.systemMessage; + } + + return output; + } +}; diff --git a/src/cli/adapters/index.ts b/src/cli/adapters/index.ts index 8c19039ed..c6b5c3a7c 100644 --- a/src/cli/adapters/index.ts +++ b/src/cli/adapters/index.ts @@ -1,16 +1,20 @@ import type { PlatformAdapter } from '../types.js'; import { claudeCodeAdapter } from './claude-code.js'; import { cursorAdapter } from './cursor.js'; +import { geminiCliAdapter } from './gemini-cli.js'; import { rawAdapter } from './raw.js'; +import { windsurfAdapter } from './windsurf.js'; export function getPlatformAdapter(platform: string): PlatformAdapter { switch (platform) { case 'claude-code': return claudeCodeAdapter; case 'cursor': return cursorAdapter; + case 'gemini-cli': return geminiCliAdapter; + case 'windsurf': return windsurfAdapter; case 'raw': return rawAdapter; // Codex CLI and other compatible platforms use the raw adapter (accepts both camelCase and snake_case fields) default: return rawAdapter; } } -export { claudeCodeAdapter, cursorAdapter, rawAdapter }; +export { claudeCodeAdapter, cursorAdapter, geminiCliAdapter, rawAdapter, windsurfAdapter }; diff --git a/src/cli/adapters/windsurf.ts b/src/cli/adapters/windsurf.ts new file mode 100644 index 000000000..7f065ab19 --- /dev/null +++ b/src/cli/adapters/windsurf.ts @@ -0,0 +1,79 @@ +import type { PlatformAdapter, NormalizedHookInput, HookResult } from '../types.js'; + +// Maps Windsurf stdin format — JSON envelope with agent_action_name + tool_info payload +// +// Common envelope (all hooks): +// { agent_action_name, trajectory_id, execution_id, timestamp, tool_info: { ... } } +// +// Event-specific tool_info payloads: +// pre_user_prompt: { user_prompt: string } +// post_write_code: { file_path, edits: [{ old_string, new_string }] } +// post_run_command: { command_line, cwd } +// post_mcp_tool_use: { mcp_server_name, mcp_tool_name, mcp_tool_arguments, mcp_result } +// post_cascade_response: { response } +export const windsurfAdapter: PlatformAdapter = { + normalizeInput(raw) { + const r = (raw ?? {}) as any; + const toolInfo = r.tool_info ?? {}; + const actionName: string = r.agent_action_name ?? ''; + + const base: NormalizedHookInput = { + sessionId: r.trajectory_id ?? r.execution_id, + cwd: toolInfo.cwd ?? process.cwd(), + platform: 'windsurf', + }; + + switch (actionName) { + case 'pre_user_prompt': + return { + ...base, + prompt: toolInfo.user_prompt, + }; + + case 'post_write_code': + return { + ...base, + toolName: 'Write', + filePath: toolInfo.file_path, + edits: toolInfo.edits, + toolInput: { + file_path: toolInfo.file_path, + edits: toolInfo.edits, + }, + }; + + case 'post_run_command': + return { + ...base, + cwd: toolInfo.cwd ?? base.cwd, + toolName: 'Bash', + toolInput: { command: toolInfo.command_line }, + }; + + case 'post_mcp_tool_use': + return { + ...base, + toolName: toolInfo.mcp_tool_name ?? 'mcp_tool', + toolInput: toolInfo.mcp_tool_arguments, + toolResponse: toolInfo.mcp_result, + }; + + case 'post_cascade_response': + return { + ...base, + toolName: 'cascade_response', + toolResponse: toolInfo.response, + }; + + default: + // Unknown action — pass through what we can + return base; + } + }, + + formatOutput(result) { + // Windsurf exit codes: 0 = success, 2 = block (pre-hooks only) + // The CLI layer handles exit codes; here we just return a simple continue flag + return { continue: result.continue ?? true }; + }, +}; diff --git a/src/integrations/opencode-plugin/index.ts b/src/integrations/opencode-plugin/index.ts new file mode 100644 index 000000000..2133a22ae --- /dev/null +++ b/src/integrations/opencode-plugin/index.ts @@ -0,0 +1,355 @@ +/** + * OpenCode Plugin for claude-mem + * + * Integrates claude-mem persistent memory with OpenCode (110k+ stars). + * Runs inside OpenCode's Bun-based plugin runtime. + * + * Plugin hooks: + * - tool.execute.after: Captures tool execution observations + * - Bus events: session.created, message.updated, session.compacted, + * file.edited, session.deleted + * + * Custom tool: + * - claude_mem_search: Search memory database from within OpenCode + */ + +// ============================================================================ +// Minimal type declarations for OpenCode Plugin SDK +// These match the runtime API provided by @opencode-ai/plugin +// ============================================================================ + +interface OpenCodeProject { + name?: string; + path?: string; +} + +interface OpenCodePluginContext { + client: unknown; + project: OpenCodeProject; + directory: string; + worktree: string; + serverUrl: URL; + $: unknown; // BunShell +} + +interface ToolExecuteAfterInput { + tool: string; + sessionID: string; + callID: string; + args: Record; +} + +interface ToolExecuteAfterOutput { + title: string; + output: string; + metadata: Record; +} + +interface ToolDefinition { + description: string; + args: Record; + execute: (args: Record, context: unknown) => Promise; +} + +// Bus event payloads +interface SessionCreatedEvent { + event: { + sessionID: string; + directory?: string; + project?: string; + }; +} + +interface MessageUpdatedEvent { + event: { + sessionID: string; + role: string; + content: string; + }; +} + +interface SessionCompactedEvent { + event: { + sessionID: string; + summary?: string; + messageCount?: number; + }; +} + +interface FileEditedEvent { + event: { + sessionID: string; + path: string; + diff?: string; + }; +} + +interface SessionDeletedEvent { + event: { + sessionID: string; + }; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const WORKER_BASE_URL = "http://127.0.0.1:37777"; +const MAX_TOOL_RESPONSE_LENGTH = 1000; + +// ============================================================================ +// Worker HTTP Client +// ============================================================================ + +async function workerPost( + path: string, + body: Record, +): Promise | null> { + try { + const response = await fetch(`${WORKER_BASE_URL}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!response.ok) { + console.warn(`[claude-mem] Worker POST ${path} returned ${response.status}`); + return null; + } + return (await response.json()) as Record; + } catch (error: unknown) { + // Gracefully handle ECONNREFUSED — worker may not be running + const message = error instanceof Error ? error.message : String(error); + if (!message.includes("ECONNREFUSED")) { + console.warn(`[claude-mem] Worker POST ${path} failed: ${message}`); + } + return null; + } +} + +function workerPostFireAndForget( + path: string, + body: Record, +): void { + fetch(`${WORKER_BASE_URL}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + if (!message.includes("ECONNREFUSED")) { + console.warn(`[claude-mem] Worker POST ${path} failed: ${message}`); + } + }); +} + +async function workerGetText(path: string): Promise { + try { + const response = await fetch(`${WORKER_BASE_URL}${path}`); + if (!response.ok) { + console.warn(`[claude-mem] Worker GET ${path} returned ${response.status}`); + return null; + } + return await response.text(); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + if (!message.includes("ECONNREFUSED")) { + console.warn(`[claude-mem] Worker GET ${path} failed: ${message}`); + } + return null; + } +} + +// ============================================================================ +// Session tracking +// ============================================================================ + +const contentSessionIdsByOpenCodeSessionId = new Map(); + +function getOrCreateContentSessionId(openCodeSessionId: string): string { + if (!contentSessionIdsByOpenCodeSessionId.has(openCodeSessionId)) { + contentSessionIdsByOpenCodeSessionId.set( + openCodeSessionId, + `opencode-${openCodeSessionId}-${Date.now()}`, + ); + } + return contentSessionIdsByOpenCodeSessionId.get(openCodeSessionId)!; +} + +// ============================================================================ +// Plugin Entry Point +// ============================================================================ + +export const ClaudeMemPlugin = async (ctx: OpenCodePluginContext) => { + const projectName = ctx.project?.name || "opencode"; + + console.log(`[claude-mem] OpenCode plugin loading (project: ${projectName})`); + + return { + // ------------------------------------------------------------------ + // Direct interceptor hooks + // ------------------------------------------------------------------ + hooks: { + tool: { + execute: { + after: ( + input: ToolExecuteAfterInput, + output: ToolExecuteAfterOutput, + ) => { + const contentSessionId = getOrCreateContentSessionId(input.sessionID); + + // Truncate long tool output + let toolResponseText = output.output || ""; + if (toolResponseText.length > MAX_TOOL_RESPONSE_LENGTH) { + toolResponseText = toolResponseText.slice(0, MAX_TOOL_RESPONSE_LENGTH); + } + + workerPostFireAndForget("/api/sessions/observations", { + contentSessionId, + tool_name: input.tool, + tool_input: input.args || {}, + tool_response: toolResponseText, + cwd: ctx.directory, + }); + }, + }, + }, + }, + + // ------------------------------------------------------------------ + // Bus event handlers + // ------------------------------------------------------------------ + event: (eventName: string, payload: unknown) => { + switch (eventName) { + case "session.created": { + const { event } = payload as SessionCreatedEvent; + const contentSessionId = getOrCreateContentSessionId(event.sessionID); + + workerPostFireAndForget("/api/sessions/init", { + contentSessionId, + project: projectName, + prompt: "", + }); + break; + } + + case "message.updated": { + const { event } = payload as MessageUpdatedEvent; + + // Only capture assistant messages as observations + if (event.role !== "assistant") break; + + const contentSessionId = getOrCreateContentSessionId(event.sessionID); + + let messageText = event.content || ""; + if (messageText.length > MAX_TOOL_RESPONSE_LENGTH) { + messageText = messageText.slice(0, MAX_TOOL_RESPONSE_LENGTH); + } + + workerPostFireAndForget("/api/sessions/observations", { + contentSessionId, + tool_name: "assistant_message", + tool_input: {}, + tool_response: messageText, + cwd: ctx.directory, + }); + break; + } + + case "session.compacted": { + const { event } = payload as SessionCompactedEvent; + const contentSessionId = getOrCreateContentSessionId(event.sessionID); + + workerPostFireAndForget("/api/sessions/summarize", { + contentSessionId, + last_assistant_message: event.summary || "", + }); + break; + } + + case "file.edited": { + const { event } = payload as FileEditedEvent; + const contentSessionId = getOrCreateContentSessionId(event.sessionID); + + workerPostFireAndForget("/api/sessions/observations", { + contentSessionId, + tool_name: "file_edit", + tool_input: { path: event.path }, + tool_response: event.diff + ? event.diff.slice(0, MAX_TOOL_RESPONSE_LENGTH) + : `File edited: ${event.path}`, + cwd: ctx.directory, + }); + break; + } + + case "session.deleted": { + const { event } = payload as SessionDeletedEvent; + const contentSessionId = contentSessionIdsByOpenCodeSessionId.get( + event.sessionID, + ); + + if (contentSessionId) { + workerPostFireAndForget("/api/sessions/complete", { + contentSessionId, + }); + contentSessionIdsByOpenCodeSessionId.delete(event.sessionID); + } + break; + } + } + }, + + // ------------------------------------------------------------------ + // Custom tools + // ------------------------------------------------------------------ + tool: { + claude_mem_search: { + description: + "Search claude-mem memory database for past observations, sessions, and context", + args: { + query: { + type: "string", + description: "Search query for memory observations", + }, + }, + async execute( + args: Record, + ): Promise { + const query = String(args.query || ""); + if (!query) { + return "Please provide a search query."; + } + + const text = await workerGetText( + `/api/search/observations?query=${encodeURIComponent(query)}&limit=10`, + ); + + if (!text) { + return "claude-mem worker is not running. Start it with: npx claude-mem start"; + } + + try { + const data = JSON.parse(text); + const items = Array.isArray(data.items) ? data.items : []; + if (items.length === 0) { + return `No results found for "${query}".`; + } + + return items + .slice(0, 10) + .map((item: Record, index: number) => { + const title = String(item.title || item.subtitle || "Untitled"); + const project = item.project ? ` [${String(item.project)}]` : ""; + return `${index + 1}. ${title}${project}`; + }) + .join("\n"); + } catch { + return "Failed to parse search results."; + } + }, + } satisfies ToolDefinition, + }, + }; +}; + +export default ClaudeMemPlugin; diff --git a/src/npx-cli/commands/ide-detection.ts b/src/npx-cli/commands/ide-detection.ts new file mode 100644 index 000000000..a97c21c19 --- /dev/null +++ b/src/npx-cli/commands/ide-detection.ts @@ -0,0 +1,172 @@ +/** + * IDE Auto-Detection + * + * Detects which AI coding IDEs / tools are installed on the system by + * probing known config directories and checking for binaries in PATH. + * + * Pure Node.js — no Bun APIs used. + */ +import { execSync } from 'child_process'; +import { existsSync, readdirSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; +import { IS_WINDOWS } from '../utils/paths.js'; + +// --------------------------------------------------------------------------- +// IDE type and metadata +// --------------------------------------------------------------------------- + +export interface IDEInfo { + /** Machine-readable identifier. */ + id: string; + /** Human-readable label for display in prompts. */ + label: string; + /** Whether the IDE was detected on this system. */ + detected: boolean; + /** Whether claude-mem has implemented setup for this IDE. */ + supported: boolean; + /** Short hint text shown in the multi-select. */ + hint?: string; +} + +// --------------------------------------------------------------------------- +// PATH helper +// --------------------------------------------------------------------------- + +function isCommandInPath(command: string): boolean { + try { + const whichCommand = IS_WINDOWS ? 'where' : 'which'; + execSync(`${whichCommand} ${command}`, { stdio: 'pipe' }); + return true; + } catch { + return false; + } +} + +// --------------------------------------------------------------------------- +// VS Code extension directory scanner +// --------------------------------------------------------------------------- + +function hasVscodeExtension(extensionNameFragment: string): boolean { + const extensionsDirectory = join(homedir(), '.vscode', 'extensions'); + if (!existsSync(extensionsDirectory)) return false; + try { + const entries = readdirSync(extensionsDirectory); + return entries.some((entry) => entry.toLowerCase().includes(extensionNameFragment.toLowerCase())); + } catch { + return false; + } +} + +// --------------------------------------------------------------------------- +// Detection map +// --------------------------------------------------------------------------- + +/** + * Detect all known IDEs and return an array of `IDEInfo` objects. + * Each entry indicates whether the IDE was found and whether claude-mem + * currently supports setting it up. + */ +export function detectInstalledIDEs(): IDEInfo[] { + const home = homedir(); + + return [ + { + id: 'claude-code', + label: 'Claude Code', + detected: existsSync(join(home, '.claude')), + supported: true, + hint: 'recommended', + }, + { + id: 'gemini-cli', + label: 'Gemini CLI', + detected: existsSync(join(home, '.gemini')), + supported: true, + }, + { + id: 'opencode', + label: 'OpenCode', + detected: + existsSync(join(home, '.config', 'opencode')) || isCommandInPath('opencode'), + supported: true, + hint: 'plugin-based integration', + }, + { + id: 'openclaw', + label: 'OpenClaw', + detected: existsSync(join(home, '.openclaw')), + supported: true, + hint: 'plugin-based integration', + }, + { + id: 'windsurf', + label: 'Windsurf', + detected: existsSync(join(home, '.codeium', 'windsurf')), + supported: true, + }, + { + id: 'codex-cli', + label: 'Codex CLI', + detected: existsSync(join(home, '.codex')), + supported: true, + hint: 'transcript-based integration', + }, + { + id: 'cursor', + label: 'Cursor', + detected: existsSync(join(home, '.cursor')), + supported: true, + }, + { + id: 'copilot-cli', + label: 'Copilot CLI', + detected: isCommandInPath('copilot'), + supported: true, + hint: 'MCP-based integration', + }, + { + id: 'antigravity', + label: 'Antigravity', + detected: existsSync(join(home, '.gemini', 'antigravity')), + supported: true, + hint: 'MCP-based integration', + }, + { + id: 'goose', + label: 'Goose', + detected: + existsSync(join(home, '.config', 'goose')) || isCommandInPath('goose'), + supported: true, + hint: 'MCP-based integration', + }, + { + id: 'crush', + label: 'Crush', + detected: isCommandInPath('crush'), + supported: true, + hint: 'MCP-based integration', + }, + { + id: 'roo-code', + label: 'Roo Code', + detected: hasVscodeExtension('roo-code'), + supported: true, + hint: 'MCP-based integration', + }, + { + id: 'warp', + label: 'Warp', + detected: existsSync(join(home, '.warp')) || isCommandInPath('warp'), + supported: true, + hint: 'MCP-based integration', + }, + ]; +} + +/** + * Return only the IDEs that were detected on this system. + */ +export function getDetectedIDEs(): IDEInfo[] { + return detectInstalledIDEs().filter((ide) => ide.detected); +} diff --git a/src/npx-cli/commands/install.ts b/src/npx-cli/commands/install.ts new file mode 100644 index 000000000..ad0cb911f --- /dev/null +++ b/src/npx-cli/commands/install.ts @@ -0,0 +1,451 @@ +/** + * Install command for `npx claude-mem install`. + * + * Replaces the git-clone + build workflow. The npm package already ships + * a pre-built `plugin/` directory; this command copies it into the right + * locations and registers it with Claude Code. + * + * Pure Node.js — no Bun APIs used. + */ +import * as p from '@clack/prompts'; +import pc from 'picocolors'; +import { execSync } from 'child_process'; +import { cpSync, existsSync, readFileSync } from 'fs'; +import { join } from 'path'; +import { + claudeSettingsPath, + ensureDirectoryExists, + installedPluginsPath, + IS_WINDOWS, + knownMarketplacesPath, + marketplaceDirectory, + npmPackagePluginDirectory, + npmPackageRootDirectory, + pluginCacheDirectory, + pluginsDirectory, + readJsonFileSafe, + readPluginVersion, + writeJsonFileAtomic, +} from '../utils/paths.js'; +import { detectInstalledIDEs } from './ide-detection.js'; + +// --------------------------------------------------------------------------- +// Registration helpers +// --------------------------------------------------------------------------- + +function registerMarketplace(): void { + const knownMarketplaces = readJsonFileSafe(knownMarketplacesPath()); + + knownMarketplaces['thedotmack'] = { + source: { + source: 'github', + repo: 'thedotmack/claude-mem', + }, + installLocation: marketplaceDirectory(), + lastUpdated: new Date().toISOString(), + autoUpdate: true, + }; + + ensureDirectoryExists(pluginsDirectory()); + writeJsonFileAtomic(knownMarketplacesPath(), knownMarketplaces); +} + +function registerPlugin(version: string): void { + const installedPlugins = readJsonFileSafe(installedPluginsPath()); + + if (!installedPlugins.version) installedPlugins.version = 2; + if (!installedPlugins.plugins) installedPlugins.plugins = {}; + + const cachePath = pluginCacheDirectory(version); + const now = new Date().toISOString(); + + installedPlugins.plugins['claude-mem@thedotmack'] = [ + { + scope: 'user', + installPath: cachePath, + version, + installedAt: now, + lastUpdated: now, + }, + ]; + + writeJsonFileAtomic(installedPluginsPath(), installedPlugins); +} + +function enablePluginInClaudeSettings(): void { + const settings = readJsonFileSafe(claudeSettingsPath()); + + if (!settings.enabledPlugins) settings.enabledPlugins = {}; + settings.enabledPlugins['claude-mem@thedotmack'] = true; + + writeJsonFileAtomic(claudeSettingsPath(), settings); +} + +// --------------------------------------------------------------------------- +// IDE setup dispatcher +// --------------------------------------------------------------------------- + +async function setupIDEs(selectedIDEs: string[]): Promise { + for (const ideId of selectedIDEs) { + switch (ideId) { + case 'claude-code': + // Claude Code picks up the plugin via marketplace registration — nothing + // else to do beyond what registerMarketplace / registerPlugin already did. + p.log.success('Claude Code: plugin registered via marketplace.'); + break; + + case 'cursor': + p.log.info('Cursor: hook configuration available after first launch.'); + p.log.info(` Run: npx claude-mem cursor-setup (coming soon)`); + break; + + case 'gemini-cli': { + const { installGeminiCliHooks } = await import('../../services/integrations/GeminiCliHooksInstaller.js'); + const geminiResult = await installGeminiCliHooks(); + if (geminiResult === 0) { + p.log.success('Gemini CLI: hooks installed.'); + } else { + p.log.error('Gemini CLI: hook installation failed.'); + } + break; + } + + case 'opencode': { + const { installOpenCodeIntegration } = await import('../../services/integrations/OpenCodeInstaller.js'); + const openCodeResult = await installOpenCodeIntegration(); + if (openCodeResult === 0) { + p.log.success('OpenCode: plugin installed.'); + } else { + p.log.error('OpenCode: plugin installation failed.'); + } + break; + } + + case 'windsurf': { + const { installWindsurfHooks } = await import('../../services/integrations/WindsurfHooksInstaller.js'); + const windsurfResult = await installWindsurfHooks(); + if (windsurfResult === 0) { + p.log.success('Windsurf: hooks installed.'); + } else { + p.log.error('Windsurf: hook installation failed.'); + } + break; + } + + case 'openclaw': { + const { installOpenClawIntegration } = await import('../../services/integrations/OpenClawInstaller.js'); + const openClawResult = await installOpenClawIntegration(); + if (openClawResult === 0) { + p.log.success('OpenClaw: plugin installed.'); + } else { + p.log.error('OpenClaw: plugin installation failed.'); + } + break; + } + + case 'codex-cli': { + const { installCodexCli } = await import('../../services/integrations/CodexCliInstaller.js'); + const codexResult = await installCodexCli(); + if (codexResult === 0) { + p.log.success('Codex CLI: transcript watching configured.'); + } else { + p.log.error('Codex CLI: integration setup failed.'); + } + break; + } + + case 'copilot-cli': + case 'antigravity': + case 'goose': + case 'crush': + case 'roo-code': + case 'warp': { + const { MCP_IDE_INSTALLERS } = await import('../../services/integrations/McpIntegrations.js'); + const mcpInstaller = MCP_IDE_INSTALLERS[ideId]; + if (mcpInstaller) { + const mcpResult = await mcpInstaller(); + const allIDEs = detectInstalledIDEs(); + const ideInfo = allIDEs.find((i) => i.id === ideId); + const ideLabel = ideInfo?.label ?? ideId; + if (mcpResult === 0) { + p.log.success(`${ideLabel}: MCP integration installed.`); + } else { + p.log.error(`${ideLabel}: MCP integration failed.`); + } + } + break; + } + + default: { + const allIDEs = detectInstalledIDEs(); + const ide = allIDEs.find((i) => i.id === ideId); + if (ide && !ide.supported) { + p.log.warn(`Support for ${ide.label} coming soon.`); + } + break; + } + } + } +} + +// --------------------------------------------------------------------------- +// Interactive IDE selection +// --------------------------------------------------------------------------- + +async function promptForIDESelection(): Promise { + const detectedIDEs = detectInstalledIDEs(); + const detected = detectedIDEs.filter((ide) => ide.detected); + + if (detected.length === 0) { + p.log.warn('No supported IDEs detected. Installing for Claude Code by default.'); + return ['claude-code']; + } + + const options = detected.map((ide) => ({ + value: ide.id, + label: ide.label, + hint: ide.supported ? ide.hint : 'coming soon', + })); + + const result = await p.multiselect({ + message: 'Which IDEs do you use?', + options, + initialValues: detected + .filter((ide) => ide.supported) + .map((ide) => ide.id), + required: true, + }); + + if (p.isCancel(result)) { + p.cancel('Installation cancelled.'); + process.exit(0); + } + + return result as string[]; +} + +// --------------------------------------------------------------------------- +// Core copy logic +// --------------------------------------------------------------------------- + +function copyPluginToMarketplace(): void { + const marketplaceDir = marketplaceDirectory(); + + ensureDirectoryExists(marketplaceDir); + + // Copy the entire npm package (not just plugin/) so that package.json, + // node_modules, and scripts are all present in the marketplace dir. + const packageRoot = npmPackageRootDirectory(); + cpSync(packageRoot, marketplaceDir, { + recursive: true, + force: true, + filter: (source) => { + // Skip .git and other unnecessary directories + if (source.includes('.git') && !source.includes('.claude-plugin')) return false; + if (source.endsWith('.tgz')) return false; + return true; + }, + }); +} + +function copyPluginToCache(version: string): void { + const sourcePluginDirectory = npmPackagePluginDirectory(); + const cachePath = pluginCacheDirectory(version); + + ensureDirectoryExists(cachePath); + cpSync(sourcePluginDirectory, cachePath, { recursive: true, force: true }); +} + +// --------------------------------------------------------------------------- +// npm install in marketplace dir +// --------------------------------------------------------------------------- + +function runNpmInstallInMarketplace(): void { + const marketplaceDir = marketplaceDirectory(); + const packageJsonPath = join(marketplaceDir, 'package.json'); + + if (!existsSync(packageJsonPath)) return; + + execSync('npm install --production', { + cwd: marketplaceDir, + stdio: 'pipe', + ...(IS_WINDOWS ? { shell: true as const } : {}), + }); +} + +// --------------------------------------------------------------------------- +// Trigger smart-install for Bun / uv +// --------------------------------------------------------------------------- + +function runSmartInstall(): void { + const smartInstallPath = join(marketplaceDirectory(), 'plugin', 'scripts', 'smart-install.js'); + + if (!existsSync(smartInstallPath)) { + p.log.warn('smart-install.js not found — skipping Bun/uv auto-install.'); + return; + } + + try { + execSync(`node "${smartInstallPath}"`, { + stdio: 'inherit', + ...(IS_WINDOWS ? { shell: true as const } : {}), + }); + } catch { + p.log.warn('smart-install encountered an issue. You may need to install Bun/uv manually.'); + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export interface InstallOptions { + /** When provided, skip the interactive IDE multi-select and use this IDE. */ + ide?: string; +} + +export async function runInstallCommand(options: InstallOptions = {}): Promise { + const version = readPluginVersion(); + + p.intro(pc.bgCyan(pc.black(' claude-mem install '))); + p.log.info(`Version: ${pc.cyan(version)}`); + p.log.info(`Platform: ${process.platform} (${process.arch})`); + + // Check for existing installation + const marketplaceDir = marketplaceDirectory(); + const alreadyInstalled = existsSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json')); + + if (alreadyInstalled) { + // Read existing version + try { + const existingPluginJson = JSON.parse( + readFileSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json'), 'utf-8'), + ); + p.log.warn(`Existing installation detected (v${existingPluginJson.version ?? 'unknown'}).`); + } catch { + p.log.warn('Existing installation detected.'); + } + + if (process.stdin.isTTY) { + const shouldContinue = await p.confirm({ + message: 'Overwrite existing installation?', + initialValue: true, + }); + + if (p.isCancel(shouldContinue) || !shouldContinue) { + p.cancel('Installation cancelled.'); + process.exit(0); + } + } + } + + // IDE selection + let selectedIDEs: string[]; + if (options.ide) { + selectedIDEs = [options.ide]; + const allIDEs = detectInstalledIDEs(); + const match = allIDEs.find((i) => i.id === options.ide); + if (match && !match.supported) { + p.log.error(`Support for ${match.label} coming soon.`); + process.exit(1); + } + if (!match) { + p.log.error(`Unknown IDE: ${options.ide}`); + p.log.info(`Available IDEs: ${allIDEs.map((i) => i.id).join(', ')}`); + process.exit(1); + } + } else if (process.stdin.isTTY) { + selectedIDEs = await promptForIDESelection(); + } else { + // Non-interactive: default to claude-code + selectedIDEs = ['claude-code']; + } + + // Run tasks + await p.tasks([ + { + title: 'Copying plugin files', + task: async (message) => { + message('Copying to marketplace directory...'); + copyPluginToMarketplace(); + return `Plugin files copied ${pc.green('OK')}`; + }, + }, + { + title: 'Caching plugin version', + task: async (message) => { + message(`Caching v${version}...`); + copyPluginToCache(version); + return `Plugin cached (v${version}) ${pc.green('OK')}`; + }, + }, + { + title: 'Registering marketplace', + task: async () => { + registerMarketplace(); + return `Marketplace registered ${pc.green('OK')}`; + }, + }, + { + title: 'Registering plugin', + task: async () => { + registerPlugin(version); + return `Plugin registered ${pc.green('OK')}`; + }, + }, + { + title: 'Enabling plugin in Claude settings', + task: async () => { + enablePluginInClaudeSettings(); + return `Plugin enabled ${pc.green('OK')}`; + }, + }, + { + title: 'Installing dependencies', + task: async (message) => { + message('Running npm install...'); + try { + runNpmInstallInMarketplace(); + return `Dependencies installed ${pc.green('OK')}`; + } catch { + return `Dependencies may need manual install ${pc.yellow('!')}`; + } + }, + }, + { + title: 'Setting up Bun and uv', + task: async (message) => { + message('Running smart-install...'); + try { + runSmartInstall(); + return `Runtime dependencies ready ${pc.green('OK')}`; + } catch { + return `Runtime setup may need attention ${pc.yellow('!')}`; + } + }, + }, + ]); + + // IDE-specific setup + await setupIDEs(selectedIDEs); + + // Summary + const summaryLines = [ + `Version: ${pc.cyan(version)}`, + `Plugin dir: ${pc.cyan(marketplaceDir)}`, + `IDEs: ${pc.cyan(selectedIDEs.join(', '))}`, + ]; + + p.note(summaryLines.join('\n'), 'Installation Complete'); + + const nextSteps = [ + 'Open Claude Code and start a conversation -- memory is automatic!', + `View your memories: ${pc.underline('http://localhost:37777')}`, + `Search past work: use ${pc.bold('/mem-search')} in Claude Code`, + `Start worker: ${pc.bold('npx claude-mem start')}`, + ]; + + p.note(nextSteps.join('\n'), 'Next Steps'); + + p.outro(pc.green('claude-mem installed successfully!')); +} diff --git a/src/npx-cli/commands/runtime.ts b/src/npx-cli/commands/runtime.ts new file mode 100644 index 000000000..c6324a58a --- /dev/null +++ b/src/npx-cli/commands/runtime.ts @@ -0,0 +1,184 @@ +/** + * Runtime command routing for `npx claude-mem start|stop|restart|status|search|transcript`. + * + * These commands delegate to the installed plugin's worker-service.cjs via Bun, + * or hit the worker's HTTP API directly (for `search`). + * + * Pure Node.js — no Bun APIs used. + */ +import { spawn } from 'child_process'; +import { existsSync } from 'fs'; +import { join } from 'path'; +import pc from 'picocolors'; +import { resolveBunBinaryPath } from '../utils/bun-resolver.js'; +import { isPluginInstalled, marketplaceDirectory } from '../utils/paths.js'; + +// --------------------------------------------------------------------------- +// Installation guard +// --------------------------------------------------------------------------- + +function ensureInstalledOrExit(): void { + if (!isPluginInstalled()) { + console.error(pc.red('claude-mem is not installed.')); + console.error(`Run: ${pc.bold('npx claude-mem install')}`); + process.exit(1); + } +} + +// --------------------------------------------------------------------------- +// Bun guard +// --------------------------------------------------------------------------- + +function resolveBunOrExit(): string { + const bunPath = resolveBunBinaryPath(); + if (!bunPath) { + console.error(pc.red('Bun not found.')); + console.error('Install Bun: https://bun.sh'); + console.error('After installation, restart your terminal.'); + process.exit(1); + } + return bunPath; +} + +// --------------------------------------------------------------------------- +// Worker-service path +// --------------------------------------------------------------------------- + +function workerServiceScriptPath(): string { + return join(marketplaceDirectory(), 'plugin', 'scripts', 'worker-service.cjs'); +} + +// --------------------------------------------------------------------------- +// Spawn helper +// --------------------------------------------------------------------------- + +function spawnBunWorkerCommand(command: string, extraArgs: string[] = []): void { + ensureInstalledOrExit(); + const bunPath = resolveBunOrExit(); + const workerScript = workerServiceScriptPath(); + + if (!existsSync(workerScript)) { + console.error(pc.red(`Worker script not found at: ${workerScript}`)); + console.error('The installation may be corrupted. Try: npx claude-mem install'); + process.exit(1); + } + + const args = [workerScript, command, ...extraArgs]; + + const child = spawn(bunPath, args, { + stdio: 'inherit', + cwd: marketplaceDirectory(), + env: process.env, + }); + + child.on('error', (error) => { + console.error(pc.red(`Failed to start Bun: ${error.message}`)); + process.exit(1); + }); + + child.on('close', (exitCode) => { + process.exit(exitCode ?? 0); + }); +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export function runStartCommand(): void { + spawnBunWorkerCommand('start'); +} + +export function runStopCommand(): void { + spawnBunWorkerCommand('stop'); +} + +export function runRestartCommand(): void { + spawnBunWorkerCommand('restart'); +} + +export function runStatusCommand(): void { + spawnBunWorkerCommand('status'); +} + +/** + * Search the worker API at `GET /api/search?q=`. + */ +export async function runSearchCommand(queryParts: string[]): Promise { + ensureInstalledOrExit(); + + const query = queryParts.join(' ').trim(); + if (!query) { + console.error(pc.red('Usage: npx claude-mem search ')); + process.exit(1); + } + + const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777'; + const searchUrl = `http://127.0.0.1:${workerPort}/api/search?q=${encodeURIComponent(query)}`; + + try { + const response = await fetch(searchUrl); + + if (!response.ok) { + if (response.status === 404) { + console.error(pc.red('Search endpoint not found. Is the worker running?')); + console.error(`Try: ${pc.bold('npx claude-mem start')}`); + process.exit(1); + } + console.error(pc.red(`Search failed: HTTP ${response.status}`)); + process.exit(1); + } + + const data = await response.json(); + + if (typeof data === 'object' && data !== null) { + console.log(JSON.stringify(data, null, 2)); + } else { + console.log(data); + } + } catch (error: any) { + if (error?.cause?.code === 'ECONNREFUSED' || error?.message?.includes('ECONNREFUSED')) { + console.error(pc.red('Worker is not running.')); + console.error(`Start it with: ${pc.bold('npx claude-mem start')}`); + process.exit(1); + } + console.error(pc.red(`Search failed: ${error.message}`)); + process.exit(1); + } +} + +/** + * Start the transcript watcher via Bun. + */ +export function runTranscriptWatchCommand(): void { + ensureInstalledOrExit(); + const bunPath = resolveBunOrExit(); + + const transcriptWatcherPath = join( + marketplaceDirectory(), + 'plugin', + 'scripts', + 'transcript-watcher.cjs', + ); + + if (!existsSync(transcriptWatcherPath)) { + // Fall back to worker-service with transcript subcommand + spawnBunWorkerCommand('transcript', ['watch']); + return; + } + + const child = spawn(bunPath, [transcriptWatcherPath, 'watch'], { + stdio: 'inherit', + cwd: marketplaceDirectory(), + env: process.env, + }); + + child.on('error', (error) => { + console.error(pc.red(`Failed to start transcript watcher: ${error.message}`)); + process.exit(1); + }); + + child.on('close', (exitCode) => { + process.exit(exitCode ?? 0); + }); +} diff --git a/src/npx-cli/commands/uninstall.ts b/src/npx-cli/commands/uninstall.ts new file mode 100644 index 000000000..431723ce5 --- /dev/null +++ b/src/npx-cli/commands/uninstall.ts @@ -0,0 +1,171 @@ +/** + * Uninstall command for `npx claude-mem uninstall`. + * + * Removes the plugin from the marketplace directory, cache, plugin + * registrations, and Claude settings. Optionally cleans up IDE-specific + * configurations. + * + * Pure Node.js — no Bun APIs used. + */ +import * as p from '@clack/prompts'; +import pc from 'picocolors'; +import { existsSync, rmSync } from 'fs'; +import { join } from 'path'; +import { + claudeSettingsPath, + installedPluginsPath, + isPluginInstalled, + knownMarketplacesPath, + marketplaceDirectory, + pluginsDirectory, + readJsonFileSafe, + writeJsonFileAtomic, +} from '../utils/paths.js'; + +// --------------------------------------------------------------------------- +// Cleanup helpers +// --------------------------------------------------------------------------- + +function removeMarketplaceDirectory(): boolean { + const marketplaceDir = marketplaceDirectory(); + if (existsSync(marketplaceDir)) { + rmSync(marketplaceDir, { recursive: true, force: true }); + return true; + } + return false; +} + +function removeCacheDirectory(): boolean { + const cacheBaseDirectory = join(pluginsDirectory(), 'cache', 'thedotmack'); + if (existsSync(cacheBaseDirectory)) { + rmSync(cacheBaseDirectory, { recursive: true, force: true }); + return true; + } + return false; +} + +function removeFromKnownMarketplaces(): void { + const knownMarketplaces = readJsonFileSafe(knownMarketplacesPath()); + if (knownMarketplaces['thedotmack']) { + delete knownMarketplaces['thedotmack']; + writeJsonFileAtomic(knownMarketplacesPath(), knownMarketplaces); + } +} + +function removeFromInstalledPlugins(): void { + const installedPlugins = readJsonFileSafe(installedPluginsPath()); + if (installedPlugins.plugins?.['claude-mem@thedotmack']) { + delete installedPlugins.plugins['claude-mem@thedotmack']; + writeJsonFileAtomic(installedPluginsPath(), installedPlugins); + } +} + +function removeFromClaudeSettings(): void { + const settings = readJsonFileSafe(claudeSettingsPath()); + if (settings.enabledPlugins?.['claude-mem@thedotmack'] !== undefined) { + delete settings.enabledPlugins['claude-mem@thedotmack']; + writeJsonFileAtomic(claudeSettingsPath(), settings); + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export async function runUninstallCommand(): Promise { + p.intro(pc.bgRed(pc.white(' claude-mem uninstall '))); + + if (!isPluginInstalled()) { + p.log.warn('claude-mem does not appear to be installed.'); + + // Still offer to clean up partial state + if (process.stdin.isTTY) { + const shouldCleanup = await p.confirm({ + message: 'Clean up any remaining registration data anyway?', + initialValue: false, + }); + + if (p.isCancel(shouldCleanup) || !shouldCleanup) { + p.outro('Nothing to do.'); + return; + } + } else { + p.outro('Nothing to do.'); + return; + } + } else if (process.stdin.isTTY) { + const shouldContinue = await p.confirm({ + message: 'Are you sure you want to uninstall claude-mem?', + initialValue: false, + }); + + if (p.isCancel(shouldContinue) || !shouldContinue) { + p.cancel('Uninstall cancelled.'); + return; + } + } + + // Stop the worker first (best-effort) + try { + const workerPort = process.env.CLAUDE_MEM_WORKER_PORT || '37777'; + await fetch(`http://127.0.0.1:${workerPort}/api/admin/shutdown`, { + method: 'POST', + signal: AbortSignal.timeout(5000), + }); + p.log.info('Worker service stopped.'); + } catch { + // Worker may not be running — that is fine + } + + await p.tasks([ + { + title: 'Removing marketplace directory', + task: async () => { + const removed = removeMarketplaceDirectory(); + return removed + ? `Marketplace directory removed ${pc.green('OK')}` + : `Marketplace directory not found ${pc.dim('skipped')}`; + }, + }, + { + title: 'Removing cache directory', + task: async () => { + const removed = removeCacheDirectory(); + return removed + ? `Cache directory removed ${pc.green('OK')}` + : `Cache directory not found ${pc.dim('skipped')}`; + }, + }, + { + title: 'Removing marketplace registration', + task: async () => { + removeFromKnownMarketplaces(); + return `Marketplace registration removed ${pc.green('OK')}`; + }, + }, + { + title: 'Removing plugin registration', + task: async () => { + removeFromInstalledPlugins(); + return `Plugin registration removed ${pc.green('OK')}`; + }, + }, + { + title: 'Removing from Claude settings', + task: async () => { + removeFromClaudeSettings(); + return `Claude settings updated ${pc.green('OK')}`; + }, + }, + ]); + + p.note( + [ + `Your data directory at ${pc.cyan('~/.claude-mem')} was preserved.`, + 'To remove it manually: rm -rf ~/.claude-mem', + ].join('\n'), + 'Note', + ); + + p.outro(pc.green('claude-mem has been uninstalled.')); +} diff --git a/src/npx-cli/index.ts b/src/npx-cli/index.ts new file mode 100644 index 000000000..68b0cd97c --- /dev/null +++ b/src/npx-cli/index.ts @@ -0,0 +1,175 @@ +#!/usr/bin/env node +/** + * NPX CLI entry point for claude-mem. + * + * Usage: + * npx claude-mem → interactive install + * npx claude-mem install → interactive install + * npx claude-mem install --ide → direct IDE setup + * npx claude-mem update → update to latest version + * npx claude-mem uninstall → remove plugin and IDE configs + * npx claude-mem version → print version + * npx claude-mem start → start worker service + * npx claude-mem stop → stop worker service + * npx claude-mem restart → restart worker service + * npx claude-mem status → show worker status + * npx claude-mem search → search observations + * npx claude-mem transcript watch → start transcript watcher + * + * This file is pure Node.js — Bun is NOT required for install commands. + * Runtime commands (`start`, `stop`, etc.) delegate to Bun via the installed plugin. + */ +import pc from 'picocolors'; +import { readPluginVersion } from './utils/paths.js'; + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +const args = process.argv.slice(2); +const command = args[0]?.toLowerCase() ?? ''; + +// --------------------------------------------------------------------------- +// Help text +// --------------------------------------------------------------------------- + +function printHelp(): void { + const version = readPluginVersion(); + + console.log(` +${pc.bold('claude-mem')} v${version} — persistent memory for AI coding assistants + +${pc.bold('Install Commands')} (no Bun required): + ${pc.cyan('npx claude-mem')} Interactive install + ${pc.cyan('npx claude-mem install')} Interactive install + ${pc.cyan('npx claude-mem install --ide ')} Install for specific IDE + ${pc.cyan('npx claude-mem update')} Update to latest version + ${pc.cyan('npx claude-mem uninstall')} Remove plugin and configs + ${pc.cyan('npx claude-mem version')} Print version + +${pc.bold('Runtime Commands')} (requires Bun, delegates to installed plugin): + ${pc.cyan('npx claude-mem start')} Start worker service + ${pc.cyan('npx claude-mem stop')} Stop worker service + ${pc.cyan('npx claude-mem restart')} Restart worker service + ${pc.cyan('npx claude-mem status')} Show worker status + ${pc.cyan('npx claude-mem search ')} Search observations + ${pc.cyan('npx claude-mem transcript watch')} Start transcript watcher + +${pc.bold('IDE Identifiers')}: + claude-code, cursor, gemini-cli, opencode, openclaw, + windsurf, codex-cli, copilot-cli, antigravity, goose, + crush, roo-code, warp +`); +} + +// --------------------------------------------------------------------------- +// Command routing +// --------------------------------------------------------------------------- + +async function main(): Promise { + switch (command) { + // -- No command: default to install ------------------------------------ + case '': { + const { runInstallCommand } = await import('./commands/install.js'); + await runInstallCommand(); + break; + } + + // -- Install ----------------------------------------------------------- + case 'install': { + const ideIndex = args.indexOf('--ide'); + const ideValue = ideIndex !== -1 ? args[ideIndex + 1] : undefined; + + const { runInstallCommand } = await import('./commands/install.js'); + await runInstallCommand({ ide: ideValue }); + break; + } + + // -- Update (alias for install — overwrite with latest) ---------------- + case 'update': + case 'upgrade': { + const { runInstallCommand } = await import('./commands/install.js'); + await runInstallCommand(); + break; + } + + // -- Uninstall --------------------------------------------------------- + case 'uninstall': + case 'remove': { + const { runUninstallCommand } = await import('./commands/uninstall.js'); + await runUninstallCommand(); + break; + } + + // -- Version ----------------------------------------------------------- + case 'version': + case '--version': + case '-v': { + console.log(readPluginVersion()); + break; + } + + // -- Help -------------------------------------------------------------- + case 'help': + case '--help': + case '-h': { + printHelp(); + break; + } + + // -- Runtime: start / stop / restart / status -------------------------- + case 'start': { + const { runStartCommand } = await import('./commands/runtime.js'); + runStartCommand(); + break; + } + case 'stop': { + const { runStopCommand } = await import('./commands/runtime.js'); + runStopCommand(); + break; + } + case 'restart': { + const { runRestartCommand } = await import('./commands/runtime.js'); + runRestartCommand(); + break; + } + case 'status': { + const { runStatusCommand } = await import('./commands/runtime.js'); + runStatusCommand(); + break; + } + + // -- Search ------------------------------------------------------------ + case 'search': { + const { runSearchCommand } = await import('./commands/runtime.js'); + await runSearchCommand(args.slice(1)); + break; + } + + // -- Transcript -------------------------------------------------------- + case 'transcript': { + const subCommand = args[1]?.toLowerCase(); + if (subCommand === 'watch') { + const { runTranscriptWatchCommand } = await import('./commands/runtime.js'); + runTranscriptWatchCommand(); + } else { + console.error(pc.red(`Unknown transcript subcommand: ${subCommand ?? '(none)'}`)); + console.error(`Usage: npx claude-mem transcript watch`); + process.exit(1); + } + break; + } + + // -- Unknown ----------------------------------------------------------- + default: { + console.error(pc.red(`Unknown command: ${command}`)); + console.error(`Run ${pc.bold('npx claude-mem --help')} for usage information.`); + process.exit(1); + } + } +} + +main().catch((error) => { + console.error(pc.red('Fatal error:'), error.message || error); + process.exit(1); +}); diff --git a/src/npx-cli/utils/bun-resolver.ts b/src/npx-cli/utils/bun-resolver.ts new file mode 100644 index 000000000..a019b7bd6 --- /dev/null +++ b/src/npx-cli/utils/bun-resolver.ts @@ -0,0 +1,85 @@ +/** + * Bun binary resolution utility. + * + * Extracted from `plugin/scripts/bun-runner.js` so that the NPX CLI + * can locate Bun without duplicating the search logic. + * + * Pure Node.js — no Bun APIs used. + */ +import { spawnSync } from 'child_process'; +import { existsSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; +import { IS_WINDOWS } from './paths.js'; + +/** + * Well-known locations where Bun might be installed, beyond PATH. + * Order matches the search priority in bun-runner.js and smart-install.js. + */ +function bunCandidatePaths(): string[] { + if (IS_WINDOWS) { + return [ + join(homedir(), '.bun', 'bin', 'bun.exe'), + join(process.env.USERPROFILE || homedir(), '.bun', 'bin', 'bun.exe'), + ]; + } + + return [ + join(homedir(), '.bun', 'bin', 'bun'), + '/usr/local/bin/bun', + '/opt/homebrew/bin/bun', + '/home/linuxbrew/.linuxbrew/bin/bun', + ]; +} + +/** + * Attempt to locate the Bun executable. + * + * 1. Check PATH via `which` / `where`. + * 2. Probe well-known installation directories. + * + * Returns the absolute path to the binary, `'bun'` if it is in PATH, + * or `null` if Bun cannot be found. + */ +export function resolveBunBinaryPath(): string | null { + // Try PATH first + const whichCommand = IS_WINDOWS ? 'where' : 'which'; + const pathCheck = spawnSync(whichCommand, ['bun'], { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + shell: IS_WINDOWS, + }); + + if (pathCheck.status === 0 && pathCheck.stdout.trim()) { + return 'bun'; // Available in PATH — use short name + } + + // Probe known install locations + for (const candidatePath of bunCandidatePaths()) { + if (existsSync(candidatePath)) { + return candidatePath; + } + } + + return null; +} + +/** + * Get the installed Bun version string (e.g. `"1.2.3"`), or `null` + * if Bun is not available. + */ +export function getBunVersionString(): string | null { + const bunPath = resolveBunBinaryPath(); + if (!bunPath) return null; + + try { + const result = spawnSync(bunPath, ['--version'], { + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + shell: IS_WINDOWS, + }); + return result.status === 0 ? result.stdout.trim() : null; + } catch { + return null; + } +} diff --git a/src/npx-cli/utils/paths.ts b/src/npx-cli/utils/paths.ts new file mode 100644 index 000000000..dbefd2c19 --- /dev/null +++ b/src/npx-cli/utils/paths.ts @@ -0,0 +1,152 @@ +/** + * Shared path utilities for the NPX CLI. + * + * All platform-specific path logic is centralized here so that every command + * resolves directories in exactly the same way, regardless of OS. + */ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { homedir } from 'os'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +// --------------------------------------------------------------------------- +// Platform detection +// --------------------------------------------------------------------------- + +export const IS_WINDOWS = process.platform === 'win32'; + +// --------------------------------------------------------------------------- +// Core paths +// --------------------------------------------------------------------------- + +/** Root of the Claude Code config directory. */ +export function claudeConfigDirectory(): string { + return process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'); +} + +/** Marketplace install directory for thedotmack. */ +export function marketplaceDirectory(): string { + return join(claudeConfigDirectory(), 'plugins', 'marketplaces', 'thedotmack'); +} + +/** Top-level plugins directory. */ +export function pluginsDirectory(): string { + return join(claudeConfigDirectory(), 'plugins'); +} + +/** Path to `known_marketplaces.json`. */ +export function knownMarketplacesPath(): string { + return join(pluginsDirectory(), 'known_marketplaces.json'); +} + +/** Path to `installed_plugins.json`. */ +export function installedPluginsPath(): string { + return join(pluginsDirectory(), 'installed_plugins.json'); +} + +/** Path to `~/.claude/settings.json`. */ +export function claudeSettingsPath(): string { + return join(claudeConfigDirectory(), 'settings.json'); +} + +/** Plugin cache directory for a specific version. */ +export function pluginCacheDirectory(version: string): string { + return join(pluginsDirectory(), 'cache', 'thedotmack', 'claude-mem', version); +} + +/** claude-mem data directory (default `~/.claude-mem`). */ +export function claudeMemDataDirectory(): string { + return join(homedir(), '.claude-mem'); +} + +// --------------------------------------------------------------------------- +// NPM package root (where the NPX package lives on disk) +// --------------------------------------------------------------------------- + +/** + * Resolve the root of the installed npm package. + * + * After bundling, the CLI entry point lives at `/dist/npx-cli/index.js`. + * Walking up 2 levels from `import.meta.url` reaches the package root + * where `plugin/` and `package.json` can be found. + */ +export function npmPackageRootDirectory(): string { + const currentFilePath = fileURLToPath(import.meta.url); + // /dist/npx-cli/index.js -> up 2 levels -> + return join(dirname(currentFilePath), '..', '..'); +} + +/** + * Path to the `plugin/` directory bundled inside the npm package. + */ +export function npmPackagePluginDirectory(): string { + return join(npmPackageRootDirectory(), 'plugin'); +} + +// --------------------------------------------------------------------------- +// Version helpers +// --------------------------------------------------------------------------- + +/** + * Read the current plugin version from the npm package's + * `plugin/.claude-plugin/plugin.json` (preferred) or from `package.json`. + */ +export function readPluginVersion(): string { + // Try plugin.json first (authoritative for plugin version) + const pluginJsonPath = join(npmPackagePluginDirectory(), '.claude-plugin', 'plugin.json'); + if (existsSync(pluginJsonPath)) { + try { + const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8')); + if (pluginJson.version) return pluginJson.version; + } catch { + // Fall through to package.json + } + } + + // Fall back to package.json at package root + const packageJsonPath = join(npmPackageRootDirectory(), 'package.json'); + if (existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + if (packageJson.version) return packageJson.version; + } catch { + // Unable to read + } + } + + return '0.0.0'; +} + +// --------------------------------------------------------------------------- +// Installation detection +// --------------------------------------------------------------------------- + +/** Returns true if the plugin appears to be installed in the marketplace dir. */ +export function isPluginInstalled(): boolean { + const marketplaceDir = marketplaceDirectory(); + return existsSync(join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json')); +} + +// --------------------------------------------------------------------------- +// JSON file helpers +// --------------------------------------------------------------------------- + +export function ensureDirectoryExists(directoryPath: string): void { + if (!existsSync(directoryPath)) { + mkdirSync(directoryPath, { recursive: true }); + } +} + +export function readJsonFileSafe(filepath: string): any { + if (!existsSync(filepath)) return {}; + try { + return JSON.parse(readFileSync(filepath, 'utf-8')); + } catch { + return {}; + } +} + +export function writeJsonFileAtomic(filepath: string, data: any): void { + ensureDirectoryExists(dirname(filepath)); + writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); +} diff --git a/src/services/integrations/CodexCliInstaller.ts b/src/services/integrations/CodexCliInstaller.ts new file mode 100644 index 000000000..9645b2bcf --- /dev/null +++ b/src/services/integrations/CodexCliInstaller.ts @@ -0,0 +1,373 @@ +/** + * CodexCliInstaller - Codex CLI integration for claude-mem + * + * Uses transcript-only watching (no notify hook). The watcher infrastructure + * already exists in src/services/transcripts/. This installer: + * + * 1. Writes/merges transcript-watch config to ~/.claude-mem/transcript-watch.json + * 2. Sets up watch for ~/.codex/sessions/**\/*.jsonl using existing watcher + * 3. Injects context via ~/.codex/AGENTS.md (Codex reads this natively) + * + * Anti-patterns: + * - Does NOT add notify hooks -- transcript watching is sufficient + * - Does NOT modify existing transcript watcher infrastructure + * - Does NOT overwrite existing transcript-watch.json -- merges only + */ + +import path from 'path'; +import { homedir } from 'os'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { logger } from '../../utils/logger.js'; +import { replaceTaggedContent } from '../../utils/claude-md-utils.js'; +import { + DEFAULT_CONFIG_PATH, + DEFAULT_STATE_PATH, + SAMPLE_CONFIG, +} from '../transcripts/config.js'; +import type { TranscriptWatchConfig, WatchTarget } from '../transcripts/types.js'; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const CODEX_DIR = path.join(homedir(), '.codex'); +const CODEX_AGENTS_MD_PATH = path.join(CODEX_DIR, 'AGENTS.md'); +const CLAUDE_MEM_DIR = path.join(homedir(), '.claude-mem'); + +/** + * The watch name used to identify the Codex CLI entry in transcript-watch.json. + * Must match the name in SAMPLE_CONFIG for merging to work correctly. + */ +const CODEX_WATCH_NAME = 'codex'; + +// --------------------------------------------------------------------------- +// Transcript Watch Config Merging +// --------------------------------------------------------------------------- + +/** + * Load existing transcript-watch.json, or return an empty config scaffold. + * Never throws -- returns a valid empty config on any parse error. + */ +function loadExistingTranscriptWatchConfig(): TranscriptWatchConfig { + const configPath = DEFAULT_CONFIG_PATH; + + if (!existsSync(configPath)) { + return { version: 1, schemas: {}, watches: [], stateFile: DEFAULT_STATE_PATH }; + } + + try { + const raw = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(raw) as TranscriptWatchConfig; + + // Ensure required fields exist + if (!parsed.version) parsed.version = 1; + if (!parsed.watches) parsed.watches = []; + if (!parsed.schemas) parsed.schemas = {}; + if (!parsed.stateFile) parsed.stateFile = DEFAULT_STATE_PATH; + + return parsed; + } catch (parseError) { + logger.error('CODEX', 'Corrupt transcript-watch.json, creating backup', { path: configPath }, parseError as Error); + + // Back up corrupt file + const backupPath = `${configPath}.backup.${Date.now()}`; + writeFileSync(backupPath, readFileSync(configPath)); + console.warn(` Backed up corrupt transcript-watch.json to ${backupPath}`); + + return { version: 1, schemas: {}, watches: [], stateFile: DEFAULT_STATE_PATH }; + } +} + +/** + * Merge Codex watch configuration into existing transcript-watch.json. + * + * - If a watch with name 'codex' already exists, it is replaced in-place. + * - If the 'codex' schema already exists, it is replaced in-place. + * - All other watches and schemas are preserved untouched. + */ +function mergeCodexWatchConfig(existingConfig: TranscriptWatchConfig): TranscriptWatchConfig { + const merged = { ...existingConfig }; + + // Merge schemas: add/replace the codex schema + merged.schemas = { ...merged.schemas }; + const codexSchema = SAMPLE_CONFIG.schemas?.[CODEX_WATCH_NAME]; + if (codexSchema) { + merged.schemas[CODEX_WATCH_NAME] = codexSchema; + } + + // Merge watches: add/replace the codex watch entry + const codexWatchFromSample = SAMPLE_CONFIG.watches.find( + (w: WatchTarget) => w.name === CODEX_WATCH_NAME, + ); + + if (codexWatchFromSample) { + const existingWatchIndex = merged.watches.findIndex( + (w: WatchTarget) => w.name === CODEX_WATCH_NAME, + ); + + if (existingWatchIndex !== -1) { + // Replace existing codex watch in-place + merged.watches[existingWatchIndex] = codexWatchFromSample; + } else { + // Append new codex watch + merged.watches.push(codexWatchFromSample); + } + } + + return merged; +} + +/** + * Write the merged transcript-watch.json config atomically. + */ +function writeTranscriptWatchConfig(config: TranscriptWatchConfig): void { + mkdirSync(CLAUDE_MEM_DIR, { recursive: true }); + writeFileSync(DEFAULT_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n'); +} + +// --------------------------------------------------------------------------- +// Context Injection (AGENTS.md) +// --------------------------------------------------------------------------- + +/** + * Inject claude-mem context section into ~/.codex/AGENTS.md. + * Uses the same tag pattern as CLAUDE.md and GEMINI.md. + * Preserves any existing user content outside the tags. + */ +function injectCodexAgentsMdContext(): void { + try { + mkdirSync(CODEX_DIR, { recursive: true }); + + let existingContent = ''; + if (existsSync(CODEX_AGENTS_MD_PATH)) { + existingContent = readFileSync(CODEX_AGENTS_MD_PATH, 'utf-8'); + } + + // Initial placeholder content -- will be populated after first session + const contextContent = [ + '# Recent Activity', + '', + '', + '', + '*No context yet. Complete your first session and context will appear here.*', + ].join('\n'); + + const finalContent = replaceTaggedContent(existingContent, contextContent); + writeFileSync(CODEX_AGENTS_MD_PATH, finalContent); + console.log(` Injected context placeholder into ${CODEX_AGENTS_MD_PATH}`); + } catch (error) { + // Non-fatal -- transcript watching still works without context injection + logger.warn('CODEX', 'Failed to inject AGENTS.md context', { error: (error as Error).message }); + console.warn(` Warning: Could not inject context into AGENTS.md: ${(error as Error).message}`); + } +} + +/** + * Remove claude-mem context section from AGENTS.md. + * Preserves user content outside the tags. + */ +function removeCodexAgentsMdContext(): void { + try { + if (!existsSync(CODEX_AGENTS_MD_PATH)) return; + + const content = readFileSync(CODEX_AGENTS_MD_PATH, 'utf-8'); + const startTag = ''; + const endTag = ''; + + const startIdx = content.indexOf(startTag); + const endIdx = content.indexOf(endTag); + + if (startIdx === -1 || endIdx === -1) return; + + // Remove the tagged section and any surrounding blank lines + const before = content.substring(0, startIdx).replace(/\n+$/, ''); + const after = content.substring(endIdx + endTag.length).replace(/^\n+/, ''); + const finalContent = (before + (after ? '\n\n' + after : '')).trim(); + + if (finalContent) { + writeFileSync(CODEX_AGENTS_MD_PATH, finalContent + '\n'); + } else { + // File would be empty -- leave it empty rather than deleting + // (user may have other tooling that expects it to exist) + writeFileSync(CODEX_AGENTS_MD_PATH, ''); + } + + console.log(` Removed context section from ${CODEX_AGENTS_MD_PATH}`); + } catch (error) { + logger.warn('CODEX', 'Failed to clean AGENTS.md context', { error: (error as Error).message }); + } +} + +// --------------------------------------------------------------------------- +// Public API: Install +// --------------------------------------------------------------------------- + +/** + * Install Codex CLI integration for claude-mem. + * + * 1. Merges Codex transcript-watch config into ~/.claude-mem/transcript-watch.json + * 2. Injects context placeholder into ~/.codex/AGENTS.md + * + * @returns 0 on success, 1 on failure + */ +export async function installCodexCli(): Promise { + console.log('\nInstalling Claude-Mem for Codex CLI (transcript watching)...\n'); + + try { + // Step 1: Merge transcript-watch config + const existingConfig = loadExistingTranscriptWatchConfig(); + const mergedConfig = mergeCodexWatchConfig(existingConfig); + writeTranscriptWatchConfig(mergedConfig); + console.log(` Updated ${DEFAULT_CONFIG_PATH}`); + console.log(` Watch path: ~/.codex/sessions/**/*.jsonl`); + console.log(` Schema: codex (v${SAMPLE_CONFIG.schemas?.codex?.version ?? '?'})`); + + // Step 2: Inject context into AGENTS.md + injectCodexAgentsMdContext(); + + console.log(` +Installation complete! + +Transcript watch config: ${DEFAULT_CONFIG_PATH} +Context file: ${CODEX_AGENTS_MD_PATH} + +How it works: + - claude-mem watches Codex session JSONL files for new activity + - No hooks needed -- transcript watching is fully automatic + - Context from past sessions is injected via ${CODEX_AGENTS_MD_PATH} + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Use Codex CLI as usual -- memory capture is automatic! +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// --------------------------------------------------------------------------- +// Public API: Uninstall +// --------------------------------------------------------------------------- + +/** + * Remove Codex CLI integration from claude-mem. + * + * 1. Removes the codex watch and schema from transcript-watch.json (preserves others) + * 2. Removes context section from AGENTS.md (preserves user content) + * + * @returns 0 on success, 1 on failure + */ +export function uninstallCodexCli(): number { + console.log('\nUninstalling Claude-Mem Codex CLI integration...\n'); + + try { + // Step 1: Remove codex watch from transcript-watch.json + if (existsSync(DEFAULT_CONFIG_PATH)) { + const config = loadExistingTranscriptWatchConfig(); + + // Remove codex watch + config.watches = config.watches.filter( + (w: WatchTarget) => w.name !== CODEX_WATCH_NAME, + ); + + // Remove codex schema + if (config.schemas) { + delete config.schemas[CODEX_WATCH_NAME]; + } + + writeTranscriptWatchConfig(config); + console.log(` Removed codex watch from ${DEFAULT_CONFIG_PATH}`); + } else { + console.log(' No transcript-watch.json found -- nothing to remove.'); + } + + // Step 2: Remove context section from AGENTS.md + removeCodexAgentsMdContext(); + + console.log('\nUninstallation complete!'); + console.log('Restart claude-mem worker to apply changes.\n'); + + return 0; + } catch (error) { + console.error(`\nUninstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// --------------------------------------------------------------------------- +// Public API: Status Check +// --------------------------------------------------------------------------- + +/** + * Check Codex CLI integration status. + * + * @returns 0 always (informational) + */ +export function checkCodexCliStatus(): number { + console.log('\nClaude-Mem Codex CLI Integration Status\n'); + + // Check transcript-watch.json + if (!existsSync(DEFAULT_CONFIG_PATH)) { + console.log('Status: Not installed'); + console.log(` No transcript watch config at ${DEFAULT_CONFIG_PATH}`); + console.log('\nRun: npx claude-mem install --ide codex-cli\n'); + return 0; + } + + try { + const config = loadExistingTranscriptWatchConfig(); + const codexWatch = config.watches.find( + (w: WatchTarget) => w.name === CODEX_WATCH_NAME, + ); + const codexSchema = config.schemas?.[CODEX_WATCH_NAME]; + + if (!codexWatch) { + console.log('Status: Not installed'); + console.log(' transcript-watch.json exists but no codex watch configured.'); + console.log('\nRun: npx claude-mem install --ide codex-cli\n'); + return 0; + } + + console.log('Status: Installed'); + console.log(` Config: ${DEFAULT_CONFIG_PATH}`); + console.log(` Watch path: ${codexWatch.path}`); + console.log(` Schema: ${codexSchema ? `codex (v${codexSchema.version ?? '?'})` : 'missing'}`); + console.log(` Start at end: ${codexWatch.startAtEnd ?? false}`); + + // Check context config + if (codexWatch.context) { + console.log(` Context mode: ${codexWatch.context.mode}`); + console.log(` Context path: ${codexWatch.context.path ?? 'default'}`); + console.log(` Context updates on: ${codexWatch.context.updateOn?.join(', ') ?? 'none'}`); + } + + // Check AGENTS.md + if (existsSync(CODEX_AGENTS_MD_PATH)) { + const mdContent = readFileSync(CODEX_AGENTS_MD_PATH, 'utf-8'); + if (mdContent.includes('')) { + console.log(` Context: Active (${CODEX_AGENTS_MD_PATH})`); + } else { + console.log(` Context: AGENTS.md exists but no context tags`); + } + } else { + console.log(` Context: No AGENTS.md file`); + } + + // Check if ~/.codex/sessions exists (indicates Codex has been used) + const sessionsDir = path.join(CODEX_DIR, 'sessions'); + if (existsSync(sessionsDir)) { + console.log(` Sessions directory: exists`); + } else { + console.log(` Sessions directory: not yet created (use Codex CLI to generate sessions)`); + } + } catch { + console.log('Status: Unknown'); + console.log(' Could not parse transcript-watch.json.'); + } + + console.log(''); + return 0; +} diff --git a/src/services/integrations/GeminiCliHooksInstaller.ts b/src/services/integrations/GeminiCliHooksInstaller.ts new file mode 100644 index 000000000..09d9ff89c --- /dev/null +++ b/src/services/integrations/GeminiCliHooksInstaller.ts @@ -0,0 +1,446 @@ +/** + * GeminiCliHooksInstaller - Gemini CLI integration for claude-mem + * + * Installs claude-mem hooks into ~/.gemini/settings.json using deep merge + * to preserve any existing user configuration. + * + * Gemini CLI hook config format: + * { + * "hooks": { + * "AfterTool": [{ + * "matcher": "*", + * "hooks": [{ "name": "claude-mem", "type": "command", "command": "...", "timeout": 5000 }] + * }] + * } + * } + * + * Events registered: + * SessionStart — session init + * BeforeAgent — capture user prompt + * AfterAgent — capture full response + * AfterTool — capture all tool results (matcher: "*") + * PreCompress — trigger summary + * SessionEnd — finalize session + */ + +import path from 'path'; +import { homedir } from 'os'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { logger } from '../../utils/logger.js'; +import { replaceTaggedContent } from '../../utils/claude-md-utils.js'; +import { findBunPath, findWorkerServicePath } from './CursorHooksInstaller.js'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface GeminiHookEntry { + name: string; + type: 'command'; + command: string; + timeout: number; +} + +interface GeminiHookMatcher { + matcher: string; + hooks: GeminiHookEntry[]; +} + +interface GeminiSettingsJson { + hooks?: Record; + [otherKeys: string]: unknown; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const GEMINI_DIR = path.join(homedir(), '.gemini'); +const GEMINI_SETTINGS_PATH = path.join(GEMINI_DIR, 'settings.json'); +const GEMINI_MD_PATH = path.join(GEMINI_DIR, 'GEMINI.md'); +const HOOK_NAME = 'claude-mem'; +const HOOK_TIMEOUT_MS = 5000; + +/** + * The Gemini CLI events we register hooks for, mapped to our internal event names. + */ +const GEMINI_EVENT_TO_CLAUDE_MEM_EVENT: Record = { + 'SessionStart': 'session-init', + 'BeforeAgent': 'user-message', + 'AfterAgent': 'observation', + 'AfterTool': 'observation', + 'PreCompress': 'summarize', + 'SessionEnd': 'session-complete', +}; + +// --------------------------------------------------------------------------- +// Deep Merge for Hook Arrays +// --------------------------------------------------------------------------- + +/** + * Merge claude-mem hooks into an existing event's hook matcher array. + * If a matcher with the same `matcher` value already has a hook named "claude-mem", + * it is replaced. Otherwise, the hook is appended. + */ +function mergeHookMatchers( + existingMatchers: GeminiHookMatcher[], + newMatcher: GeminiHookMatcher, +): GeminiHookMatcher[] { + const result = [...existingMatchers]; + + const existingMatcherIndex = result.findIndex( + (m) => m.matcher === newMatcher.matcher, + ); + + if (existingMatcherIndex !== -1) { + // Matcher exists — replace or add our hook within it + const existing = result[existingMatcherIndex]; + const hookIndex = existing.hooks.findIndex((h) => h.name === HOOK_NAME); + if (hookIndex !== -1) { + existing.hooks[hookIndex] = newMatcher.hooks[0]; + } else { + existing.hooks.push(newMatcher.hooks[0]); + } + } else { + // No matching matcher — add the whole entry + result.push(newMatcher); + } + + return result; +} + +// --------------------------------------------------------------------------- +// Hook Installation +// --------------------------------------------------------------------------- + +/** + * Build the hook command string for a given Gemini CLI event. + * + * Invokes: hook gemini-cli + */ +function buildHookCommand(bunPath: string, workerServicePath: string, claudeMemEvent: string): string { + const escapedBunPath = bunPath.replace(/\\/g, '\\\\'); + const escapedWorkerPath = workerServicePath.replace(/\\/g, '\\\\'); + return `"${escapedBunPath}" "${escapedWorkerPath}" hook gemini-cli ${claudeMemEvent}`; +} + +/** + * Install claude-mem hooks into Gemini CLI's settings.json. + * Deep-merges with existing configuration — never overwrites. + * + * @returns 0 on success, 1 on failure + */ +export async function installGeminiCliHooks(): Promise { + console.log('\nInstalling Claude-Mem Gemini CLI hooks...\n'); + + // Find required paths + const workerServicePath = findWorkerServicePath(); + if (!workerServicePath) { + console.error('Could not find worker-service.cjs'); + console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs'); + return 1; + } + + const bunPath = findBunPath(); + console.log(` Using Bun runtime: ${bunPath}`); + console.log(` Worker service: ${workerServicePath}`); + + try { + // Ensure ~/.gemini exists + mkdirSync(GEMINI_DIR, { recursive: true }); + + // Read existing settings (deep merge, never overwrite) + let settings: GeminiSettingsJson = {}; + if (existsSync(GEMINI_SETTINGS_PATH)) { + try { + settings = JSON.parse(readFileSync(GEMINI_SETTINGS_PATH, 'utf-8')); + } catch (parseError) { + logger.error('GEMINI', 'Corrupt settings.json, creating backup', { path: GEMINI_SETTINGS_PATH }, parseError as Error); + // Back up corrupt file + const backupPath = `${GEMINI_SETTINGS_PATH}.backup.${Date.now()}`; + writeFileSync(backupPath, readFileSync(GEMINI_SETTINGS_PATH)); + console.warn(` Backed up corrupt settings.json to ${backupPath}`); + settings = {}; + } + } + + // Initialize hooks object if missing + if (!settings.hooks) { + settings.hooks = {}; + } + + // Register each event + for (const [geminiEvent, claudeMemEvent] of Object.entries(GEMINI_EVENT_TO_CLAUDE_MEM_EVENT)) { + const command = buildHookCommand(bunPath, workerServicePath, claudeMemEvent); + + // AfterTool uses matcher: "*" to capture all tool results + const matcherValue = geminiEvent === 'AfterTool' ? '*' : '*'; + + const newMatcher: GeminiHookMatcher = { + matcher: matcherValue, + hooks: [{ + name: HOOK_NAME, + type: 'command', + command, + timeout: HOOK_TIMEOUT_MS, + }], + }; + + const existingMatchers = settings.hooks[geminiEvent] ?? []; + settings.hooks[geminiEvent] = mergeHookMatchers(existingMatchers, newMatcher); + } + + // Write merged settings + writeFileSync(GEMINI_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n'); + console.log(` Updated ${GEMINI_SETTINGS_PATH}`); + console.log(` Registered hooks for: ${Object.keys(GEMINI_EVENT_TO_CLAUDE_MEM_EVENT).join(', ')}`); + + // Inject context into GEMINI.md + injectGeminiMdContext(); + + console.log(` +Installation complete! + +Hooks installed to: ${GEMINI_SETTINGS_PATH} +Using unified CLI: bun worker-service.cjs hook gemini-cli + +Next steps: + 1. Start claude-mem worker: claude-mem start + 2. Restart Gemini CLI to load the hooks + 3. Memory capture is now automatic! + +Context Injection: + Context from past sessions is injected via ${GEMINI_MD_PATH} + and automatically included in every Gemini CLI session. +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// --------------------------------------------------------------------------- +// Context Injection (GEMINI.md) +// --------------------------------------------------------------------------- + +/** + * Inject claude-mem context section into ~/.gemini/GEMINI.md. + * Uses the same tag pattern as CLAUDE.md. + * Preserves any existing user content outside the tags. + */ +function injectGeminiMdContext(): void { + try { + let existingContent = ''; + if (existsSync(GEMINI_MD_PATH)) { + existingContent = readFileSync(GEMINI_MD_PATH, 'utf-8'); + } + + // Initial placeholder content — will be populated after first session + const contextContent = [ + '# Recent Activity', + '', + '', + '', + '*No context yet. Complete your first session and context will appear here.*', + ].join('\n'); + + const finalContent = replaceTaggedContent(existingContent, contextContent); + writeFileSync(GEMINI_MD_PATH, finalContent); + console.log(` Injected context placeholder into ${GEMINI_MD_PATH}`); + } catch (error) { + // Non-fatal — hooks still work without context injection + logger.warn('GEMINI', 'Failed to inject GEMINI.md context', { error: (error as Error).message }); + console.warn(` Warning: Could not inject context into GEMINI.md: ${(error as Error).message}`); + } +} + +// --------------------------------------------------------------------------- +// Uninstallation +// --------------------------------------------------------------------------- + +/** + * Remove claude-mem hooks from Gemini CLI settings.json. + * Preserves all other hooks and settings. + * + * @returns 0 on success, 1 on failure + */ +export function uninstallGeminiCliHooks(): number { + console.log('\nUninstalling Claude-Mem Gemini CLI hooks...\n'); + + try { + if (!existsSync(GEMINI_SETTINGS_PATH)) { + console.log(' No settings.json found — nothing to uninstall.'); + return 0; + } + + let settings: GeminiSettingsJson; + try { + settings = JSON.parse(readFileSync(GEMINI_SETTINGS_PATH, 'utf-8')); + } catch { + console.error(' Could not parse settings.json'); + return 1; + } + + if (!settings.hooks) { + console.log(' No hooks configured — nothing to uninstall.'); + return 0; + } + + let removedCount = 0; + + // Remove claude-mem hooks from each event + for (const eventName of Object.keys(settings.hooks)) { + const matchers = settings.hooks[eventName]; + if (!Array.isArray(matchers)) continue; + + for (const matcher of matchers) { + if (!Array.isArray(matcher.hooks)) continue; + const beforeLength = matcher.hooks.length; + matcher.hooks = matcher.hooks.filter((h) => h.name !== HOOK_NAME); + removedCount += beforeLength - matcher.hooks.length; + } + + // Clean up empty matchers + settings.hooks[eventName] = matchers.filter( + (m) => m.hooks.length > 0, + ); + + // Clean up empty event arrays + if (settings.hooks[eventName].length === 0) { + delete settings.hooks[eventName]; + } + } + + // Clean up empty hooks object + if (Object.keys(settings.hooks).length === 0) { + delete settings.hooks; + } + + writeFileSync(GEMINI_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n'); + console.log(` Removed ${removedCount} claude-mem hook(s) from settings.json`); + + // Remove context section from GEMINI.md + removeGeminiMdContext(); + + console.log('\nUninstallation complete!'); + console.log('Restart Gemini CLI to apply changes.\n'); + + return 0; + } catch (error) { + console.error(`\nUninstallation failed: ${(error as Error).message}`); + return 1; + } +} + +/** + * Remove claude-mem context section from GEMINI.md. + * Preserves user content outside the tags. + */ +function removeGeminiMdContext(): void { + try { + if (!existsSync(GEMINI_MD_PATH)) return; + + const content = readFileSync(GEMINI_MD_PATH, 'utf-8'); + const startTag = ''; + const endTag = ''; + + const startIdx = content.indexOf(startTag); + const endIdx = content.indexOf(endTag); + + if (startIdx === -1 || endIdx === -1) return; + + // Remove the tagged section and any surrounding blank lines + const before = content.substring(0, startIdx).replace(/\n+$/, ''); + const after = content.substring(endIdx + endTag.length).replace(/^\n+/, ''); + const finalContent = (before + (after ? '\n\n' + after : '')).trim(); + + if (finalContent) { + writeFileSync(GEMINI_MD_PATH, finalContent + '\n'); + } else { + // File would be empty — leave it empty rather than deleting + // (user may have other tooling that expects it to exist) + writeFileSync(GEMINI_MD_PATH, ''); + } + + console.log(` Removed context section from ${GEMINI_MD_PATH}`); + } catch (error) { + logger.warn('GEMINI', 'Failed to clean GEMINI.md context', { error: (error as Error).message }); + } +} + +// --------------------------------------------------------------------------- +// Status Check +// --------------------------------------------------------------------------- + +/** + * Check Gemini CLI hooks installation status. + * + * @returns 0 always (informational) + */ +export function checkGeminiCliHooksStatus(): number { + console.log('\nClaude-Mem Gemini CLI Hooks Status\n'); + + if (!existsSync(GEMINI_SETTINGS_PATH)) { + console.log('Status: Not installed'); + console.log(` No settings file at ${GEMINI_SETTINGS_PATH}`); + console.log('\nRun: npx claude-mem install --ide gemini-cli\n'); + return 0; + } + + try { + const settings: GeminiSettingsJson = JSON.parse(readFileSync(GEMINI_SETTINGS_PATH, 'utf-8')); + + if (!settings.hooks) { + console.log('Status: Not installed'); + console.log(' settings.json exists but has no hooks section.'); + return 0; + } + + const installedEvents: string[] = []; + for (const [eventName, matchers] of Object.entries(settings.hooks)) { + if (!Array.isArray(matchers)) continue; + for (const matcher of matchers) { + if (matcher.hooks?.some((h: GeminiHookEntry) => h.name === HOOK_NAME)) { + installedEvents.push(eventName); + } + } + } + + if (installedEvents.length === 0) { + console.log('Status: Not installed'); + console.log(' settings.json exists but no claude-mem hooks found.'); + } else { + console.log('Status: Installed'); + console.log(` Config: ${GEMINI_SETTINGS_PATH}`); + console.log(` Events: ${installedEvents.join(', ')}`); + + // Check GEMINI.md context + if (existsSync(GEMINI_MD_PATH)) { + const mdContent = readFileSync(GEMINI_MD_PATH, 'utf-8'); + if (mdContent.includes('')) { + console.log(` Context: Active (${GEMINI_MD_PATH})`); + } else { + console.log(` Context: GEMINI.md exists but no context tags`); + } + } else { + console.log(` Context: No GEMINI.md file`); + } + + // Check expected vs actual events + const expectedEvents = Object.keys(GEMINI_EVENT_TO_CLAUDE_MEM_EVENT); + const missingEvents = expectedEvents.filter((e) => !installedEvents.includes(e)); + if (missingEvents.length > 0) { + console.log(` Warning: Missing events: ${missingEvents.join(', ')}`); + console.log(' Run install again to add missing hooks.'); + } + } + } catch { + console.log('Status: Unknown'); + console.log(' Could not parse settings.json.'); + } + + console.log(''); + return 0; +} diff --git a/src/services/integrations/McpIntegrations.ts b/src/services/integrations/McpIntegrations.ts new file mode 100644 index 000000000..75a6f3474 --- /dev/null +++ b/src/services/integrations/McpIntegrations.ts @@ -0,0 +1,610 @@ +/** + * McpIntegrations - MCP-based IDE integrations for claude-mem + * + * Handles MCP config writing and context injection for IDEs that support + * the Model Context Protocol. These are "MCP-only" integrations: they provide + * search tools and context injection but do NOT capture transcripts. + * + * Supported IDEs: + * - Copilot CLI + * - Antigravity (Gemini) + * - Goose + * - Crush + * - Roo Code + * - Warp + * + * All IDEs point to the same MCP server: plugin/scripts/mcp-server.cjs + */ + +import path from 'path'; +import { homedir } from 'os'; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { logger } from '../../utils/logger.js'; +import { findMcpServerPath } from './CursorHooksInstaller.js'; + +// ============================================================================ +// Shared Constants +// ============================================================================ + +const CONTEXT_TAG_OPEN = ''; +const CONTEXT_TAG_CLOSE = ''; + +const PLACEHOLDER_CONTEXT = `# claude-mem: Cross-Session Memory + +*No context yet. Complete your first session and context will appear here.* + +Use claude-mem's MCP search tools for manual memory queries.`; + +// ============================================================================ +// Shared Utilities +// ============================================================================ + +/** + * Build the standard MCP server entry that all IDEs use. + * Points to the same mcp-server.cjs script. + */ +function buildMcpServerEntry(mcpServerPath: string): { command: string; args: string[] } { + return { + command: 'node', + args: [mcpServerPath], + }; +} + +/** + * Read a JSON file safely, returning a default value if it doesn't exist or is corrupt. + */ +function readJsonSafe(filePath: string, defaultValue: T): T { + if (!existsSync(filePath)) return defaultValue; + try { + return JSON.parse(readFileSync(filePath, 'utf-8')); + } catch (error) { + logger.error('MCP', `Corrupt JSON file, using default`, { path: filePath }, error as Error); + return defaultValue; + } +} + +/** + * Inject or update a section in a markdown file. + * Creates the file if it doesn't exist. Preserves content outside the tags. + */ +function injectContextIntoMarkdownFile(filePath: string, contextContent: string): void { + const parentDirectory = path.dirname(filePath); + mkdirSync(parentDirectory, { recursive: true }); + + const wrappedContent = `${CONTEXT_TAG_OPEN}\n${contextContent}\n${CONTEXT_TAG_CLOSE}`; + + if (existsSync(filePath)) { + let existingContent = readFileSync(filePath, 'utf-8'); + + const tagStartIndex = existingContent.indexOf(CONTEXT_TAG_OPEN); + const tagEndIndex = existingContent.indexOf(CONTEXT_TAG_CLOSE); + + if (tagStartIndex !== -1 && tagEndIndex !== -1) { + // Replace existing section + existingContent = + existingContent.slice(0, tagStartIndex) + + wrappedContent + + existingContent.slice(tagEndIndex + CONTEXT_TAG_CLOSE.length); + } else { + // Append section + existingContent = existingContent.trimEnd() + '\n\n' + wrappedContent + '\n'; + } + + writeFileSync(filePath, existingContent, 'utf-8'); + } else { + writeFileSync(filePath, wrappedContent + '\n', 'utf-8'); + } +} + +/** + * Write a standard MCP JSON config file, merging with existing config. + * Supports both { "mcpServers": { ... } } and { "servers": { ... } } formats. + */ +function writeMcpJsonConfig( + configFilePath: string, + mcpServerPath: string, + serversKeyName: string = 'mcpServers', +): void { + const parentDirectory = path.dirname(configFilePath); + mkdirSync(parentDirectory, { recursive: true }); + + const existingConfig = readJsonSafe>(configFilePath, {}); + + if (!existingConfig[serversKeyName]) { + existingConfig[serversKeyName] = {}; + } + + existingConfig[serversKeyName]['claude-mem'] = buildMcpServerEntry(mcpServerPath); + + writeFileSync(configFilePath, JSON.stringify(existingConfig, null, 2) + '\n'); +} + +// ============================================================================ +// Copilot CLI +// ============================================================================ + +/** + * Get the Copilot CLI MCP config path. + * Copilot CLI uses ~/.github/copilot/mcp.json for user-level MCP config. + */ +function getCopilotCliMcpConfigPath(): string { + return path.join(homedir(), '.github', 'copilot', 'mcp.json'); +} + +/** + * Get the Copilot CLI context injection path for the current workspace. + * Copilot reads instructions from .github/copilot-instructions.md in the workspace. + */ +function getCopilotCliContextPath(): string { + return path.join(process.cwd(), '.github', 'copilot-instructions.md'); +} + +/** + * Install claude-mem MCP integration for Copilot CLI. + * + * - Writes MCP config to ~/.github/copilot/mcp.json + * - Injects context into .github/copilot-instructions.md in the workspace + * + * @returns 0 on success, 1 on failure + */ +export async function installCopilotCliMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Copilot CLI...\n'); + + const mcpServerPath = findMcpServerPath(); + if (!mcpServerPath) { + console.error('Could not find MCP server script'); + console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs'); + return 1; + } + + try { + // Write MCP config — Copilot CLI uses { "servers": { ... } } format + const configPath = getCopilotCliMcpConfigPath(); + writeMcpJsonConfig(configPath, mcpServerPath, 'servers'); + console.log(` MCP config written to: ${configPath}`); + + // Inject context into workspace instructions + const contextPath = getCopilotCliContextPath(); + injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); + console.log(` Context placeholder written to: ${contextPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} +Context: ${contextPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Copilot CLI. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Copilot CLI to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Antigravity +// ============================================================================ + +/** + * Get the Antigravity MCP config path. + * Antigravity stores MCP config at ~/.gemini/antigravity/mcp_config.json. + */ +function getAntigravityMcpConfigPath(): string { + return path.join(homedir(), '.gemini', 'antigravity', 'mcp_config.json'); +} + +/** + * Get the Antigravity context injection path for the current workspace. + * Antigravity reads agent rules from .agent/rules/ in the workspace. + */ +function getAntigravityContextPath(): string { + return path.join(process.cwd(), '.agent', 'rules', 'claude-mem-context.md'); +} + +/** + * Install claude-mem MCP integration for Antigravity. + * + * - Writes MCP config to ~/.gemini/antigravity/mcp_config.json + * - Injects context into .agent/rules/claude-mem-context.md in the workspace + * + * @returns 0 on success, 1 on failure + */ +export async function installAntigravityMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Antigravity...\n'); + + const mcpServerPath = findMcpServerPath(); + if (!mcpServerPath) { + console.error('Could not find MCP server script'); + console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs'); + return 1; + } + + try { + // Write MCP config + const configPath = getAntigravityMcpConfigPath(); + writeMcpJsonConfig(configPath, mcpServerPath); + console.log(` MCP config written to: ${configPath}`); + + // Inject context into workspace rules + const contextPath = getAntigravityContextPath(); + injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); + console.log(` Context placeholder written to: ${contextPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} +Context: ${contextPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Antigravity. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Antigravity to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Goose +// ============================================================================ + +/** + * Get the Goose config path. + * Goose stores its config at ~/.config/goose/config.yaml. + */ +function getGooseConfigPath(): string { + return path.join(homedir(), '.config', 'goose', 'config.yaml'); +} + +/** + * Check if a YAML string already has a claude-mem entry under mcpServers. + * Uses string matching to avoid needing a YAML parser. + */ +function gooseConfigHasClaudeMemEntry(yamlContent: string): boolean { + // Look for "claude-mem:" indented under mcpServers + return yamlContent.includes('claude-mem:') && + yamlContent.includes('mcpServers:'); +} + +/** + * Build the Goose YAML MCP server block as a string. + * Produces properly indented YAML without needing a parser. + */ +function buildGooseMcpYamlBlock(mcpServerPath: string): string { + // Goose expects the mcpServers section at the top level + return [ + 'mcpServers:', + ' claude-mem:', + ' command: node', + ' args:', + ` - ${mcpServerPath}`, + ].join('\n'); +} + +/** + * Build just the claude-mem server entry (for appending under existing mcpServers). + */ +function buildGooseClaudeMemEntryYaml(mcpServerPath: string): string { + return [ + ' claude-mem:', + ' command: node', + ' args:', + ` - ${mcpServerPath}`, + ].join('\n'); +} + +/** + * Install claude-mem MCP integration for Goose. + * + * - Writes/merges MCP config into ~/.config/goose/config.yaml + * - Uses string manipulation for YAML (no parser dependency) + * + * @returns 0 on success, 1 on failure + */ +export async function installGooseMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Goose...\n'); + + const mcpServerPath = findMcpServerPath(); + if (!mcpServerPath) { + console.error('Could not find MCP server script'); + console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs'); + return 1; + } + + try { + const configPath = getGooseConfigPath(); + const configDirectory = path.dirname(configPath); + mkdirSync(configDirectory, { recursive: true }); + + if (existsSync(configPath)) { + let yamlContent = readFileSync(configPath, 'utf-8'); + + if (gooseConfigHasClaudeMemEntry(yamlContent)) { + // Already configured — replace the claude-mem block + // Find the claude-mem entry and replace it + const claudeMemPattern = /( {2}claude-mem:\n(?:.*\n)*?(?= {2}\S|\n\n|$))/; + const newEntry = buildGooseClaudeMemEntryYaml(mcpServerPath) + '\n'; + + if (claudeMemPattern.test(yamlContent)) { + yamlContent = yamlContent.replace(claudeMemPattern, newEntry); + } + writeFileSync(configPath, yamlContent); + console.log(` Updated existing claude-mem entry in: ${configPath}`); + } else if (yamlContent.includes('mcpServers:')) { + // mcpServers section exists but no claude-mem entry — append under it + const mcpServersIndex = yamlContent.indexOf('mcpServers:'); + const insertionPoint = mcpServersIndex + 'mcpServers:'.length; + const newEntry = '\n' + buildGooseClaudeMemEntryYaml(mcpServerPath); + + yamlContent = + yamlContent.slice(0, insertionPoint) + + newEntry + + yamlContent.slice(insertionPoint); + + writeFileSync(configPath, yamlContent); + console.log(` Added claude-mem to existing mcpServers in: ${configPath}`); + } else { + // No mcpServers section — append the entire block + const mcpBlock = '\n' + buildGooseMcpYamlBlock(mcpServerPath) + '\n'; + yamlContent = yamlContent.trimEnd() + '\n' + mcpBlock; + writeFileSync(configPath, yamlContent); + console.log(` Appended mcpServers section to: ${configPath}`); + } + } else { + // File doesn't exist — create from template + const templateContent = buildGooseMcpYamlBlock(mcpServerPath) + '\n'; + writeFileSync(configPath, templateContent); + console.log(` Created config with MCP server: ${configPath}`); + } + + console.log(` +Installation complete! + +MCP config: ${configPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Goose. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Goose to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Crush +// ============================================================================ + +/** + * Get the Crush MCP config path. + * Crush stores MCP config at ~/.config/crush/mcp.json. + */ +function getCrushMcpConfigPath(): string { + return path.join(homedir(), '.config', 'crush', 'mcp.json'); +} + +/** + * Install claude-mem MCP integration for Crush. + * + * - Writes MCP config to ~/.config/crush/mcp.json + * + * @returns 0 on success, 1 on failure + */ +export async function installCrushMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Crush...\n'); + + const mcpServerPath = findMcpServerPath(); + if (!mcpServerPath) { + console.error('Could not find MCP server script'); + console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs'); + return 1; + } + + try { + // Write MCP config + const configPath = getCrushMcpConfigPath(); + writeMcpJsonConfig(configPath, mcpServerPath); + console.log(` MCP config written to: ${configPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Crush. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Crush to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Roo Code +// ============================================================================ + +/** + * Get the Roo Code MCP config path for the current workspace. + * Roo Code reads MCP config from .roo/mcp.json in the workspace. + */ +function getRooCodeMcpConfigPath(): string { + return path.join(process.cwd(), '.roo', 'mcp.json'); +} + +/** + * Get the Roo Code context injection path for the current workspace. + * Roo Code reads rules from .roo/rules/ in the workspace. + */ +function getRooCodeContextPath(): string { + return path.join(process.cwd(), '.roo', 'rules', 'claude-mem-context.md'); +} + +/** + * Install claude-mem MCP integration for Roo Code. + * + * - Writes MCP config to .roo/mcp.json in the workspace + * - Injects context into .roo/rules/claude-mem-context.md in the workspace + * + * @returns 0 on success, 1 on failure + */ +export async function installRooCodeMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Roo Code...\n'); + + const mcpServerPath = findMcpServerPath(); + if (!mcpServerPath) { + console.error('Could not find MCP server script'); + console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs'); + return 1; + } + + try { + // Write MCP config to workspace + const configPath = getRooCodeMcpConfigPath(); + writeMcpJsonConfig(configPath, mcpServerPath); + console.log(` MCP config written to: ${configPath}`); + + // Inject context into workspace rules + const contextPath = getRooCodeContextPath(); + injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); + console.log(` Context placeholder written to: ${contextPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} +Context: ${contextPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Roo Code. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Roo Code to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Warp +// ============================================================================ + +/** + * Get the Warp context injection path for the current workspace. + * Warp reads project-level instructions from WARP.md in the project root. + */ +function getWarpContextPath(): string { + return path.join(process.cwd(), 'WARP.md'); +} + +/** + * Get the Warp MCP config path. + * Warp stores MCP config at ~/.warp/mcp.json when supported. + */ +function getWarpMcpConfigPath(): string { + return path.join(homedir(), '.warp', 'mcp.json'); +} + +/** + * Install claude-mem MCP integration for Warp. + * + * - Writes MCP config to ~/.warp/mcp.json + * - Injects context into WARP.md in the project root + * + * @returns 0 on success, 1 on failure + */ +export async function installWarpMcpIntegration(): Promise { + console.log('\nInstalling Claude-Mem MCP integration for Warp...\n'); + + const mcpServerPath = findMcpServerPath(); + if (!mcpServerPath) { + console.error('Could not find MCP server script'); + console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs'); + return 1; + } + + try { + // Write MCP config — Warp may also support configuring MCP via Warp Drive UI + const configPath = getWarpMcpConfigPath(); + if (existsSync(path.dirname(configPath))) { + writeMcpJsonConfig(configPath, mcpServerPath); + console.log(` MCP config written to: ${configPath}`); + } else { + console.log(` Note: ~/.warp/ not found. MCP may need to be configured via Warp Drive UI.`); + } + + // Inject context into project-level WARP.md + const contextPath = getWarpContextPath(); + injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); + console.log(` Context placeholder written to: ${contextPath}`); + + console.log(` +Installation complete! + +MCP config: ${configPath} +Context: ${contextPath} + +Note: This is an MCP-only integration providing search tools and context. +Transcript capture is not available for Warp. +If MCP config via file is not supported, configure MCP through Warp Drive UI. + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart Warp to pick up the MCP server +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +// ============================================================================ +// Unified Installer (used by npx install command) +// ============================================================================ + +/** + * Map of IDE identifiers to their install functions. + * Used by the install command to dispatch to the correct integration. + */ +export const MCP_IDE_INSTALLERS: Record Promise> = { + 'copilot-cli': installCopilotCliMcpIntegration, + 'antigravity': installAntigravityMcpIntegration, + 'goose': installGooseMcpIntegration, + 'crush': installCrushMcpIntegration, + 'roo-code': installRooCodeMcpIntegration, + 'warp': installWarpMcpIntegration, +}; diff --git a/src/services/integrations/OpenClawInstaller.ts b/src/services/integrations/OpenClawInstaller.ts new file mode 100644 index 000000000..0867f7948 --- /dev/null +++ b/src/services/integrations/OpenClawInstaller.ts @@ -0,0 +1,430 @@ +/** + * OpenClawInstaller - OpenClaw gateway integration installer for claude-mem + * + * Installs the pre-built claude-mem plugin into OpenClaw's extension directory + * and registers it in ~/.openclaw/openclaw.json. + * + * Install strategy: File-based + * - Copies the pre-built plugin from the npm package's openclaw/dist/ directory + * to ~/.openclaw/extensions/claude-mem/dist/ + * - Registers the plugin in openclaw.json under plugins.entries.claude-mem + * - Sets the memory slot to claude-mem + * + * Important: The OpenClaw plugin ships pre-built from the npm package. + * It must NOT be rebuilt at install time. + */ + +import path from 'path'; +import { homedir } from 'os'; +import { + existsSync, + readFileSync, + writeFileSync, + mkdirSync, + cpSync, + rmSync, + unlinkSync, +} from 'fs'; +import { logger } from '../../utils/logger.js'; + +// ============================================================================ +// Path Resolution +// ============================================================================ + +/** + * Resolve the OpenClaw config directory (~/.openclaw). + */ +export function getOpenClawConfigDirectory(): string { + return path.join(homedir(), '.openclaw'); +} + +/** + * Resolve the OpenClaw extensions directory where plugins are installed. + */ +export function getOpenClawExtensionsDirectory(): string { + return path.join(getOpenClawConfigDirectory(), 'extensions'); +} + +/** + * Resolve the claude-mem extension install directory. + */ +export function getOpenClawClaudeMemExtensionDirectory(): string { + return path.join(getOpenClawExtensionsDirectory(), 'claude-mem'); +} + +/** + * Resolve the path to openclaw.json config file. + */ +export function getOpenClawConfigFilePath(): string { + return path.join(getOpenClawConfigDirectory(), 'openclaw.json'); +} + +// ============================================================================ +// Pre-built Plugin Location +// ============================================================================ + +/** + * Find the pre-built OpenClaw plugin bundle in the npm package. + * Searches in: openclaw/dist/index.js relative to package root, + * then the marketplace install location. + */ +export function findPreBuiltPluginDirectory(): string | null { + const possibleRoots = [ + // Marketplace install location (production — after `npx claude-mem install`) + path.join( + process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), '.claude'), + 'plugins', 'marketplaces', 'thedotmack', + ), + // Development location (relative to project root) + process.cwd(), + ]; + + for (const root of possibleRoots) { + const openclawDistDirectory = path.join(root, 'openclaw', 'dist'); + const pluginEntryPoint = path.join(openclawDistDirectory, 'index.js'); + if (existsSync(pluginEntryPoint)) { + return openclawDistDirectory; + } + } + + return null; +} + +/** + * Find the openclaw.plugin.json file for copying alongside the plugin. + */ +export function findPluginManifestPath(): string | null { + const possibleRoots = [ + path.join( + process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), '.claude'), + 'plugins', 'marketplaces', 'thedotmack', + ), + process.cwd(), + ]; + + for (const root of possibleRoots) { + const manifestPath = path.join(root, 'openclaw', 'openclaw.plugin.json'); + if (existsSync(manifestPath)) { + return manifestPath; + } + } + + return null; +} + +/** + * Find the openclaw skills directory for copying alongside the plugin. + */ +export function findPluginSkillsDirectory(): string | null { + const possibleRoots = [ + path.join( + process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), '.claude'), + 'plugins', 'marketplaces', 'thedotmack', + ), + process.cwd(), + ]; + + for (const root of possibleRoots) { + const skillsDirectory = path.join(root, 'openclaw', 'skills'); + if (existsSync(skillsDirectory)) { + return skillsDirectory; + } + } + + return null; +} + +// ============================================================================ +// OpenClaw Config (openclaw.json) Management +// ============================================================================ + +/** + * Read openclaw.json safely, returning an empty object if missing or invalid. + */ +function readOpenClawConfig(): Record { + const configFilePath = getOpenClawConfigFilePath(); + if (!existsSync(configFilePath)) return {}; + try { + return JSON.parse(readFileSync(configFilePath, 'utf-8')); + } catch { + return {}; + } +} + +/** + * Write openclaw.json atomically, creating the directory if needed. + */ +function writeOpenClawConfig(config: Record): void { + const configDirectory = getOpenClawConfigDirectory(); + mkdirSync(configDirectory, { recursive: true }); + writeFileSync(getOpenClawConfigFilePath(), JSON.stringify(config, null, 2) + '\n', 'utf-8'); +} + +/** + * Register claude-mem in openclaw.json by merging into the existing config. + * Does NOT overwrite the entire file -- only touches the claude-mem entry + * and the memory slot. + */ +function registerPluginInOpenClawConfig( + workerPort: number = 37777, + project: string = 'openclaw', + syncMemoryFile: boolean = true, +): void { + const config = readOpenClawConfig(); + + // Ensure the plugins structure exists + if (!config.plugins) config.plugins = {}; + if (!config.plugins.slots) config.plugins.slots = {}; + if (!config.plugins.entries) config.plugins.entries = {}; + + // Set the memory slot to claude-mem + config.plugins.slots.memory = 'claude-mem'; + + // Create or update the claude-mem plugin entry + if (!config.plugins.entries['claude-mem']) { + config.plugins.entries['claude-mem'] = { + enabled: true, + config: { + workerPort, + project, + syncMemoryFile, + }, + }; + } else { + // Merge: enable and update config without losing existing user settings + config.plugins.entries['claude-mem'].enabled = true; + if (!config.plugins.entries['claude-mem'].config) { + config.plugins.entries['claude-mem'].config = {}; + } + const existingPluginConfig = config.plugins.entries['claude-mem'].config; + // Only set defaults if not already configured + if (existingPluginConfig.workerPort === undefined) existingPluginConfig.workerPort = workerPort; + if (existingPluginConfig.project === undefined) existingPluginConfig.project = project; + if (existingPluginConfig.syncMemoryFile === undefined) existingPluginConfig.syncMemoryFile = syncMemoryFile; + } + + writeOpenClawConfig(config); +} + +/** + * Remove claude-mem from openclaw.json without deleting other config. + */ +function unregisterPluginFromOpenClawConfig(): void { + const configFilePath = getOpenClawConfigFilePath(); + if (!existsSync(configFilePath)) return; + + const config = readOpenClawConfig(); + + // Remove claude-mem entry + if (config.plugins?.entries?.['claude-mem']) { + delete config.plugins.entries['claude-mem']; + } + + // Clear memory slot if it points to claude-mem + if (config.plugins?.slots?.memory === 'claude-mem') { + delete config.plugins.slots.memory; + } + + writeOpenClawConfig(config); +} + +// ============================================================================ +// Plugin Installation +// ============================================================================ + +/** + * Install the claude-mem plugin into OpenClaw's extensions directory. + * Copies the pre-built plugin bundle and registers it in openclaw.json. + * + * @returns 0 on success, 1 on failure + */ +export function installOpenClawPlugin(): number { + const preBuiltDistDirectory = findPreBuiltPluginDirectory(); + if (!preBuiltDistDirectory) { + console.error('Could not find pre-built OpenClaw plugin bundle.'); + console.error(' Expected at: openclaw/dist/index.js'); + console.error(' Ensure the npm package includes the openclaw directory.'); + return 1; + } + + const extensionDirectory = getOpenClawClaudeMemExtensionDirectory(); + const destinationDistDirectory = path.join(extensionDirectory, 'dist'); + + try { + // Create the extension directory structure + mkdirSync(destinationDistDirectory, { recursive: true }); + + // Copy pre-built dist files + cpSync(preBuiltDistDirectory, destinationDistDirectory, { recursive: true, force: true }); + console.log(` Plugin dist copied to: ${destinationDistDirectory}`); + + // Copy openclaw.plugin.json if available + const manifestPath = findPluginManifestPath(); + if (manifestPath) { + const destinationManifest = path.join(extensionDirectory, 'openclaw.plugin.json'); + cpSync(manifestPath, destinationManifest, { force: true }); + console.log(` Plugin manifest copied to: ${destinationManifest}`); + } + + // Copy skills directory if available + const skillsDirectory = findPluginSkillsDirectory(); + if (skillsDirectory) { + const destinationSkills = path.join(extensionDirectory, 'skills'); + cpSync(skillsDirectory, destinationSkills, { recursive: true, force: true }); + console.log(` Skills copied to: ${destinationSkills}`); + } + + // Create a minimal package.json for the extension (OpenClaw expects this) + const extensionPackageJson = { + name: 'claude-mem', + version: '1.0.0', + type: 'module', + main: 'dist/index.js', + openclaw: { extensions: ['./dist/index.js'] }, + }; + writeFileSync( + path.join(extensionDirectory, 'package.json'), + JSON.stringify(extensionPackageJson, null, 2) + '\n', + 'utf-8', + ); + + // Register in openclaw.json (merge, not overwrite) + registerPluginInOpenClawConfig(); + console.log(` Registered in openclaw.json`); + + logger.info('OPENCLAW', 'Plugin installed', { destination: extensionDirectory }); + return 0; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Failed to install OpenClaw plugin: ${message}`); + return 1; + } +} + +// ============================================================================ +// Uninstallation +// ============================================================================ + +/** + * Remove the claude-mem plugin from OpenClaw. + * Removes extension files and unregisters from openclaw.json. + * + * @returns 0 on success, 1 on failure + */ +export function uninstallOpenClawPlugin(): number { + let hasErrors = false; + + // Remove extension directory + const extensionDirectory = getOpenClawClaudeMemExtensionDirectory(); + if (existsSync(extensionDirectory)) { + try { + rmSync(extensionDirectory, { recursive: true, force: true }); + console.log(` Removed extension: ${extensionDirectory}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(` Failed to remove extension directory: ${message}`); + hasErrors = true; + } + } + + // Unregister from openclaw.json + try { + unregisterPluginFromOpenClawConfig(); + console.log(` Unregistered from openclaw.json`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(` Failed to update openclaw.json: ${message}`); + hasErrors = true; + } + + return hasErrors ? 1 : 0; +} + +// ============================================================================ +// Status Check +// ============================================================================ + +/** + * Check OpenClaw integration status. + * + * @returns 0 always (informational only) + */ +export function checkOpenClawStatus(): number { + console.log('\nClaude-Mem OpenClaw Integration Status\n'); + + const configDirectory = getOpenClawConfigDirectory(); + const extensionDirectory = getOpenClawClaudeMemExtensionDirectory(); + const configFilePath = getOpenClawConfigFilePath(); + const pluginEntryPoint = path.join(extensionDirectory, 'dist', 'index.js'); + + console.log(`Config directory: ${configDirectory}`); + console.log(` Exists: ${existsSync(configDirectory) ? 'yes' : 'no'}`); + console.log(''); + + console.log(`Extension directory: ${extensionDirectory}`); + console.log(` Exists: ${existsSync(extensionDirectory) ? 'yes' : 'no'}`); + console.log(` Plugin entry: ${existsSync(pluginEntryPoint) ? 'yes' : 'no'}`); + console.log(''); + + console.log(`Config (openclaw.json): ${configFilePath}`); + if (existsSync(configFilePath)) { + const config = readOpenClawConfig(); + const isRegistered = config.plugins?.entries?.['claude-mem'] !== undefined; + const isEnabled = config.plugins?.entries?.['claude-mem']?.enabled === true; + const isMemorySlot = config.plugins?.slots?.memory === 'claude-mem'; + + console.log(` Exists: yes`); + console.log(` Registered: ${isRegistered ? 'yes' : 'no'}`); + console.log(` Enabled: ${isEnabled ? 'yes' : 'no'}`); + console.log(` Memory slot: ${isMemorySlot ? 'yes' : 'no'}`); + + if (isRegistered) { + const pluginConfig = config.plugins.entries['claude-mem'].config; + if (pluginConfig) { + console.log(` Worker port: ${pluginConfig.workerPort ?? 'default'}`); + console.log(` Project: ${pluginConfig.project ?? 'default'}`); + console.log(` Sync MEMORY.md: ${pluginConfig.syncMemoryFile ?? 'default'}`); + } + } + } else { + console.log(` Exists: no`); + } + + console.log(''); + return 0; +} + +// ============================================================================ +// Full Install Flow (used by npx install command) +// ============================================================================ + +/** + * Run the full OpenClaw installation: copy plugin + register in config. + * + * @returns 0 on success, 1 on failure + */ +export async function installOpenClawIntegration(): Promise { + console.log('\nInstalling Claude-Mem for OpenClaw...\n'); + + // Step 1: Install plugin files and register in config + const pluginResult = installOpenClawPlugin(); + if (pluginResult !== 0) { + return pluginResult; + } + + const extensionDirectory = getOpenClawClaudeMemExtensionDirectory(); + + console.log(` +Installation complete! + +Plugin installed to: ${extensionDirectory} +Config updated: ${getOpenClawConfigFilePath()} + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart OpenClaw to load the plugin + 3. Memory capture is automatic from then on +`); + + return 0; +} diff --git a/src/services/integrations/OpenCodeInstaller.ts b/src/services/integrations/OpenCodeInstaller.ts new file mode 100644 index 000000000..1ac34fbbb --- /dev/null +++ b/src/services/integrations/OpenCodeInstaller.ts @@ -0,0 +1,373 @@ +/** + * OpenCodeInstaller - OpenCode IDE integration installer for claude-mem + * + * Installs the claude-mem plugin into OpenCode's plugin directory and + * sets up context injection via AGENTS.md. + * + * Install strategy: File-based (Option A) + * - Copies the built plugin to the OpenCode plugins directory + * - Plugins in that directory are auto-loaded at startup + * + * Context injection: + * - Appends/updates section in AGENTS.md + * + * Respects OPENCODE_CONFIG_DIR env var for config directory resolution. + */ + +import path from 'path'; +import { homedir } from 'os'; +import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from 'fs'; +import { logger } from '../../utils/logger.js'; + +// ============================================================================ +// Path Resolution +// ============================================================================ + +/** + * Resolve the OpenCode config directory. + * Respects OPENCODE_CONFIG_DIR env var, falls back to ~/.config/opencode. + */ +export function getOpenCodeConfigDirectory(): string { + if (process.env.OPENCODE_CONFIG_DIR) { + return process.env.OPENCODE_CONFIG_DIR; + } + return path.join(homedir(), '.config', 'opencode'); +} + +/** + * Resolve the OpenCode plugins directory. + */ +export function getOpenCodePluginsDirectory(): string { + return path.join(getOpenCodeConfigDirectory(), 'plugins'); +} + +/** + * Resolve the AGENTS.md path for context injection. + */ +export function getOpenCodeAgentsMdPath(): string { + return path.join(getOpenCodeConfigDirectory(), 'AGENTS.md'); +} + +/** + * Resolve the path to the installed plugin file. + */ +export function getInstalledPluginPath(): string { + return path.join(getOpenCodePluginsDirectory(), 'claude-mem.js'); +} + +// ============================================================================ +// Plugin Installation +// ============================================================================ + +/** + * Find the built OpenCode plugin bundle. + * Searches in: dist/opencode-plugin/index.js (built output), + * then marketplace location. + */ +export function findBuiltPluginPath(): string | null { + const possiblePaths = [ + // Marketplace install location (production) + path.join( + process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), '.claude'), + 'plugins', 'marketplaces', 'thedotmack', + 'dist', 'opencode-plugin', 'index.js', + ), + // Development location (relative to project root) + path.join(process.cwd(), 'dist', 'opencode-plugin', 'index.js'), + ]; + + for (const candidatePath of possiblePaths) { + if (existsSync(candidatePath)) { + return candidatePath; + } + } + + return null; +} + +/** + * Install the claude-mem plugin into OpenCode's plugins directory. + * Copies the built plugin bundle to ~/.config/opencode/plugins/claude-mem.js + * + * @returns 0 on success, 1 on failure + */ +export function installOpenCodePlugin(): number { + const builtPluginPath = findBuiltPluginPath(); + if (!builtPluginPath) { + console.error('Could not find built OpenCode plugin bundle.'); + console.error(' Expected at: dist/opencode-plugin/index.js'); + console.error(' Run the build first: npm run build'); + return 1; + } + + const pluginsDirectory = getOpenCodePluginsDirectory(); + const destinationPath = getInstalledPluginPath(); + + try { + // Create plugins directory if needed + mkdirSync(pluginsDirectory, { recursive: true }); + + // Copy plugin bundle + copyFileSync(builtPluginPath, destinationPath); + + console.log(` Plugin installed to: ${destinationPath}`); + logger.info('OPENCODE', 'Plugin installed', { destination: destinationPath }); + + return 0; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Failed to install OpenCode plugin: ${message}`); + return 1; + } +} + +// ============================================================================ +// Context Injection (AGENTS.md) +// ============================================================================ + +const CONTEXT_TAG_OPEN = ''; +const CONTEXT_TAG_CLOSE = ''; + +/** + * Inject or update claude-mem context in OpenCode's AGENTS.md file. + * + * If the file doesn't exist, creates it with the context section. + * If the file exists, replaces the existing section + * or appends one at the end. + * + * @param contextContent - The context content to inject (without tags) + * @returns 0 on success, 1 on failure + */ +export function injectContextIntoAgentsMd(contextContent: string): number { + const agentsMdPath = getOpenCodeAgentsMdPath(); + const wrappedContent = `${CONTEXT_TAG_OPEN}\n${contextContent}\n${CONTEXT_TAG_CLOSE}`; + + try { + const configDirectory = getOpenCodeConfigDirectory(); + mkdirSync(configDirectory, { recursive: true }); + + if (existsSync(agentsMdPath)) { + let existingContent = readFileSync(agentsMdPath, 'utf-8'); + + // Check if context tags already exist + const tagStartIndex = existingContent.indexOf(CONTEXT_TAG_OPEN); + const tagEndIndex = existingContent.indexOf(CONTEXT_TAG_CLOSE); + + if (tagStartIndex !== -1 && tagEndIndex !== -1) { + // Replace existing section + existingContent = + existingContent.slice(0, tagStartIndex) + + wrappedContent + + existingContent.slice(tagEndIndex + CONTEXT_TAG_CLOSE.length); + } else { + // Append section + existingContent = existingContent.trimEnd() + '\n\n' + wrappedContent + '\n'; + } + + writeFileSync(agentsMdPath, existingContent, 'utf-8'); + } else { + // Create new AGENTS.md with context + const newContent = `# Claude-Mem Memory Context\n\n${wrappedContent}\n`; + writeFileSync(agentsMdPath, newContent, 'utf-8'); + } + + logger.info('OPENCODE', 'Context injected into AGENTS.md', { path: agentsMdPath }); + return 0; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Failed to inject context into AGENTS.md: ${message}`); + return 1; + } +} + +/** + * Sync context from the worker into OpenCode's AGENTS.md. + * Fetches context from the worker API and writes it to AGENTS.md. + * + * @param port - Worker port number + * @param project - Project name for context filtering + */ +export async function syncContextToAgentsMd( + port: number, + project: string, +): Promise { + try { + const response = await fetch( + `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`, + ); + + if (!response.ok) return; + + const contextText = await response.text(); + if (contextText && contextText.trim()) { + injectContextIntoAgentsMd(contextText); + } + } catch { + // Worker not available — non-critical + } +} + +// ============================================================================ +// Uninstallation +// ============================================================================ + +/** + * Remove the claude-mem plugin from OpenCode. + * Removes the plugin file and cleans up the AGENTS.md context section. + * + * @returns 0 on success, 1 on failure + */ +export function uninstallOpenCodePlugin(): number { + let hasErrors = false; + + // Remove plugin file + const pluginPath = getInstalledPluginPath(); + if (existsSync(pluginPath)) { + try { + unlinkSync(pluginPath); + console.log(` Removed plugin: ${pluginPath}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(` Failed to remove plugin: ${message}`); + hasErrors = true; + } + } + + // Remove context section from AGENTS.md + const agentsMdPath = getOpenCodeAgentsMdPath(); + if (existsSync(agentsMdPath)) { + try { + let content = readFileSync(agentsMdPath, 'utf-8'); + const tagStartIndex = content.indexOf(CONTEXT_TAG_OPEN); + const tagEndIndex = content.indexOf(CONTEXT_TAG_CLOSE); + + if (tagStartIndex !== -1 && tagEndIndex !== -1) { + content = + content.slice(0, tagStartIndex).trimEnd() + + '\n' + + content.slice(tagEndIndex + CONTEXT_TAG_CLOSE.length).trimStart(); + + // If the file is now essentially empty, don't bother keeping it + if (content.trim().length === 0) { + unlinkSync(agentsMdPath); + console.log(` Removed empty AGENTS.md`); + } else { + writeFileSync(agentsMdPath, content.trimEnd() + '\n', 'utf-8'); + console.log(` Cleaned context from AGENTS.md`); + } + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(` Failed to clean AGENTS.md: ${message}`); + hasErrors = true; + } + } + + return hasErrors ? 1 : 0; +} + +// ============================================================================ +// Status Check +// ============================================================================ + +/** + * Check OpenCode integration status. + * + * @returns 0 always (informational only) + */ +export function checkOpenCodeStatus(): number { + console.log('\nClaude-Mem OpenCode Integration Status\n'); + + const configDirectory = getOpenCodeConfigDirectory(); + const pluginPath = getInstalledPluginPath(); + const agentsMdPath = getOpenCodeAgentsMdPath(); + + console.log(`Config directory: ${configDirectory}`); + console.log(` Exists: ${existsSync(configDirectory) ? 'yes' : 'no'}`); + console.log(''); + + console.log(`Plugin: ${pluginPath}`); + console.log(` Installed: ${existsSync(pluginPath) ? 'yes' : 'no'}`); + console.log(''); + + console.log(`Context (AGENTS.md): ${agentsMdPath}`); + if (existsSync(agentsMdPath)) { + const content = readFileSync(agentsMdPath, 'utf-8'); + const hasContextTags = content.includes(CONTEXT_TAG_OPEN); + console.log(` Exists: yes`); + console.log(` Has claude-mem context: ${hasContextTags ? 'yes' : 'no'}`); + } else { + console.log(` Exists: no`); + } + + console.log(''); + return 0; +} + +// ============================================================================ +// Full Install Flow (used by npx install command) +// ============================================================================ + +/** + * Run the full OpenCode installation: plugin + context injection. + * + * @returns 0 on success, 1 on failure + */ +export async function installOpenCodeIntegration(): Promise { + console.log('\nInstalling Claude-Mem for OpenCode...\n'); + + // Step 1: Install plugin + const pluginResult = installOpenCodePlugin(); + if (pluginResult !== 0) { + return pluginResult; + } + + // Step 2: Create initial context in AGENTS.md + const placeholderContext = `# Memory Context from Past Sessions + +*No context yet. Complete your first session and context will appear here.* + +Use claude-mem search tools for manual memory queries.`; + + // Try to fetch real context from worker first + try { + const healthResponse = await fetch('http://127.0.0.1:37777/api/readiness'); + if (healthResponse.ok) { + const contextResponse = await fetch( + `http://127.0.0.1:37777/api/context/inject?project=opencode`, + ); + if (contextResponse.ok) { + const realContext = await contextResponse.text(); + if (realContext && realContext.trim()) { + injectContextIntoAgentsMd(realContext); + console.log(' Context injected from existing memory'); + } else { + injectContextIntoAgentsMd(placeholderContext); + console.log(' Placeholder context created (will populate after first session)'); + } + } else { + injectContextIntoAgentsMd(placeholderContext); + } + } else { + injectContextIntoAgentsMd(placeholderContext); + console.log(' Placeholder context created (worker not running)'); + } + } catch { + injectContextIntoAgentsMd(placeholderContext); + console.log(' Placeholder context created (worker not running)'); + } + + console.log(` +Installation complete! + +Plugin installed to: ${getInstalledPluginPath()} +Context file: ${getOpenCodeAgentsMdPath()} + +Next steps: + 1. Start claude-mem worker: npx claude-mem start + 2. Restart OpenCode to load the plugin + 3. Memory capture is automatic from then on +`); + + return 0; +} diff --git a/src/services/integrations/WindsurfHooksInstaller.ts b/src/services/integrations/WindsurfHooksInstaller.ts new file mode 100644 index 000000000..2cfd05c67 --- /dev/null +++ b/src/services/integrations/WindsurfHooksInstaller.ts @@ -0,0 +1,520 @@ +/** + * WindsurfHooksInstaller - Windsurf IDE integration for claude-mem + * + * Handles: + * - Windsurf hooks installation/uninstallation to ~/.codeium/windsurf/hooks.json + * - Context file generation (.windsurf/rules/claude-mem-context.md) + * - Project registry management for auto-context updates + * + * Windsurf hooks.json format: + * { + * "hooks": { + * "": [{ "command": "...", "show_output": false, "working_directory": "..." }] + * } + * } + * + * Events registered (all post-action, non-blocking): + * - pre_user_prompt — session init + context injection + * - post_write_code — code generation observation + * - post_run_command — command execution observation + * - post_mcp_tool_use — MCP tool results + * - post_cascade_response — full AI response + */ + +import path from 'path'; +import { homedir } from 'os'; +import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, renameSync } from 'fs'; +import { logger } from '../../utils/logger.js'; +import { getWorkerPort } from '../../shared/worker-utils.js'; +import { DATA_DIR } from '../../shared/paths.js'; +import { findBunPath, findWorkerServicePath } from './CursorHooksInstaller.js'; + +// ============================================================================ +// Types +// ============================================================================ + +interface WindsurfHookEntry { + command: string; + show_output: boolean; + working_directory: string; +} + +interface WindsurfHooksJson { + hooks: { + [eventName: string]: WindsurfHookEntry[]; + }; +} + +interface WindsurfProjectRegistry { + [projectName: string]: { + workspacePath: string; + installedAt: string; + }; +} + +// ============================================================================ +// Constants +// ============================================================================ + +/** User-level hooks config — global coverage across all Windsurf workspaces */ +const WINDSURF_HOOKS_DIR = path.join(homedir(), '.codeium', 'windsurf'); +const WINDSURF_HOOKS_JSON_PATH = path.join(WINDSURF_HOOKS_DIR, 'hooks.json'); + +/** Windsurf context rule limit: 6,000 chars per file */ +const WINDSURF_CONTEXT_CHAR_LIMIT = 6000; + +/** Registry file for tracking projects with Windsurf hooks */ +const WINDSURF_REGISTRY_FILE = path.join(DATA_DIR, 'windsurf-projects.json'); + +/** Hook events we register */ +const WINDSURF_HOOK_EVENTS = [ + 'pre_user_prompt', + 'post_write_code', + 'post_run_command', + 'post_mcp_tool_use', + 'post_cascade_response', +] as const; + +// ============================================================================ +// Project Registry +// ============================================================================ + +/** + * Read the Windsurf project registry + */ +export function readWindsurfRegistry(): WindsurfProjectRegistry { + try { + if (!existsSync(WINDSURF_REGISTRY_FILE)) return {}; + return JSON.parse(readFileSync(WINDSURF_REGISTRY_FILE, 'utf-8')); + } catch (error) { + logger.error('WINDSURF', 'Failed to read registry, using empty', { + file: WINDSURF_REGISTRY_FILE, + }, error as Error); + return {}; + } +} + +/** + * Write the Windsurf project registry + */ +export function writeWindsurfRegistry(registry: WindsurfProjectRegistry): void { + const dir = path.dirname(WINDSURF_REGISTRY_FILE); + mkdirSync(dir, { recursive: true }); + writeFileSync(WINDSURF_REGISTRY_FILE, JSON.stringify(registry, null, 2)); +} + +/** + * Register a project for auto-context updates + */ +export function registerWindsurfProject(projectName: string, workspacePath: string): void { + const registry = readWindsurfRegistry(); + registry[projectName] = { + workspacePath, + installedAt: new Date().toISOString(), + }; + writeWindsurfRegistry(registry); + logger.info('WINDSURF', 'Registered project for auto-context updates', { projectName, workspacePath }); +} + +/** + * Unregister a project from auto-context updates + */ +export function unregisterWindsurfProject(projectName: string): void { + const registry = readWindsurfRegistry(); + if (registry[projectName]) { + delete registry[projectName]; + writeWindsurfRegistry(registry); + logger.info('WINDSURF', 'Unregistered project', { projectName }); + } +} + +/** + * Update Windsurf context files for all registered projects matching this project name. + * Called by SDK agents after saving a summary. + */ +export async function updateWindsurfContextForProject(projectName: string, port: number): Promise { + const registry = readWindsurfRegistry(); + const entry = registry[projectName]; + + if (!entry) return; // Project doesn't have Windsurf hooks installed + + try { + const response = await fetch( + `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(projectName)}` + ); + + if (!response.ok) return; + + const context = await response.text(); + if (!context || !context.trim()) return; + + writeWindsurfContextFile(entry.workspacePath, context); + logger.debug('WINDSURF', 'Updated context file', { projectName, workspacePath: entry.workspacePath }); + } catch (error) { + // Background context update — failure is non-critical + logger.error('WINDSURF', 'Failed to update context file', { projectName }, error as Error); + } +} + +// ============================================================================ +// Context File +// ============================================================================ + +/** + * Write context to the workspace-level Windsurf rules directory. + * Windsurf rules are workspace-scoped: .windsurf/rules/claude-mem-context.md + * Rule file limit: 6,000 chars per file. + */ +export function writeWindsurfContextFile(workspacePath: string, context: string): void { + const rulesDir = path.join(workspacePath, '.windsurf', 'rules'); + const rulesFile = path.join(rulesDir, 'claude-mem-context.md'); + const tempFile = `${rulesFile}.tmp`; + + mkdirSync(rulesDir, { recursive: true }); + + let content = `# Memory Context from Past Sessions + +The following context is from claude-mem, a persistent memory system that tracks your coding sessions. + +${context} + +--- +*Auto-updated by claude-mem after each session. Use MCP search tools for detailed queries.* +`; + + // Enforce Windsurf's 6K char limit + if (content.length > WINDSURF_CONTEXT_CHAR_LIMIT) { + content = content.slice(0, WINDSURF_CONTEXT_CHAR_LIMIT - 50) + + '\n\n*[Truncated — use MCP search for full history]*\n'; + } + + // Atomic write: temp file + rename + writeFileSync(tempFile, content); + renameSync(tempFile, rulesFile); +} + +// ============================================================================ +// Hook Installation +// ============================================================================ + +/** + * Build the hook command string for a given event. + * Uses bun to run worker-service.cjs with the windsurf platform adapter. + */ +function buildHookCommand(bunPath: string, workerServicePath: string, eventName: string): string { + // Map Windsurf event names to unified CLI hook commands + const eventToCommand: Record = { + 'pre_user_prompt': 'session-init', + 'post_write_code': 'file-edit', + 'post_run_command': 'observation', + 'post_mcp_tool_use': 'observation', + 'post_cascade_response': 'observation', + }; + + const hookCommand = eventToCommand[eventName] ?? 'observation'; + + // Escape backslashes for JSON on Windows + const escapedBunPath = bunPath.replace(/\\/g, '\\\\'); + const escapedWorkerPath = workerServicePath.replace(/\\/g, '\\\\'); + + return `"${escapedBunPath}" "${escapedWorkerPath}" hook windsurf ${hookCommand}`; +} + +/** + * Read existing hooks.json, merge our hooks, and write back. + * Preserves any existing hooks from other tools. + */ +function mergeAndWriteHooksJson( + bunPath: string, + workerServicePath: string, + workingDirectory: string, +): void { + mkdirSync(WINDSURF_HOOKS_DIR, { recursive: true }); + + // Read existing hooks.json if present + let existingConfig: WindsurfHooksJson = { hooks: {} }; + if (existsSync(WINDSURF_HOOKS_JSON_PATH)) { + try { + existingConfig = JSON.parse(readFileSync(WINDSURF_HOOKS_JSON_PATH, 'utf-8')); + if (!existingConfig.hooks) { + existingConfig.hooks = {}; + } + } catch (error) { + logger.error('WINDSURF', 'Corrupt hooks.json, starting fresh', { + path: WINDSURF_HOOKS_JSON_PATH, + }, error as Error); + existingConfig = { hooks: {} }; + } + } + + // For each event, add our hook entry (remove any previous claude-mem entries first) + for (const eventName of WINDSURF_HOOK_EVENTS) { + const command = buildHookCommand(bunPath, workerServicePath, eventName); + + const hookEntry: WindsurfHookEntry = { + command, + show_output: false, + working_directory: workingDirectory, + }; + + // Get existing hooks for this event, filtering out old claude-mem ones + const existingHooks = (existingConfig.hooks[eventName] ?? []).filter( + (hook) => !hook.command.includes('worker-service') || !hook.command.includes('windsurf') + ); + + existingConfig.hooks[eventName] = [...existingHooks, hookEntry]; + } + + writeFileSync(WINDSURF_HOOKS_JSON_PATH, JSON.stringify(existingConfig, null, 2)); +} + +/** + * Install Windsurf hooks to ~/.codeium/windsurf/hooks.json (user-level). + * Merges with existing hooks.json to preserve other integrations. + */ +export async function installWindsurfHooks(): Promise { + console.log('\nInstalling Claude-Mem Windsurf hooks (user level)...\n'); + + // Find the worker-service.cjs path + const workerServicePath = findWorkerServicePath(); + if (!workerServicePath) { + console.error('Could not find worker-service.cjs'); + console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs'); + return 1; + } + + // Find bun executable — required because worker-service.cjs uses bun:sqlite + const bunPath = findBunPath(); + + // IMPORTANT: Tilde expansion is NOT supported in working_directory — use absolute paths + const workingDirectory = path.dirname(workerServicePath); + + try { + console.log(` Using Bun runtime: ${bunPath}`); + console.log(` Worker service: ${workerServicePath}`); + + // Merge our hooks into the existing hooks.json + mergeAndWriteHooksJson(bunPath, workerServicePath, workingDirectory); + console.log(` Created/merged hooks.json`); + + // Set up initial context for the current workspace + const workspaceRoot = process.cwd(); + await setupWindsurfProjectContext(workspaceRoot); + + console.log(` +Installation complete! + +Hooks installed to: ${WINDSURF_HOOKS_JSON_PATH} +Using unified CLI: bun worker-service.cjs hook windsurf + +Events registered: + - pre_user_prompt (session init + context injection) + - post_write_code (code generation observation) + - post_run_command (command execution observation) + - post_mcp_tool_use (MCP tool results) + - post_cascade_response (full AI response) + +Next steps: + 1. Start claude-mem worker: claude-mem start + 2. Restart Windsurf to load the hooks + 3. Context is injected via .windsurf/rules/claude-mem-context.md (workspace-level) +`); + + return 0; + } catch (error) { + console.error(`\nInstallation failed: ${(error as Error).message}`); + return 1; + } +} + +/** + * Setup initial context file for a Windsurf workspace + */ +async function setupWindsurfProjectContext(workspaceRoot: string): Promise { + const port = getWorkerPort(); + const projectName = path.basename(workspaceRoot); + let contextGenerated = false; + + console.log(` Generating initial context...`); + + try { + const healthResponse = await fetch(`http://127.0.0.1:${port}/api/readiness`); + if (healthResponse.ok) { + const contextResponse = await fetch( + `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(projectName)}` + ); + if (contextResponse.ok) { + const context = await contextResponse.text(); + if (context && context.trim()) { + writeWindsurfContextFile(workspaceRoot, context); + contextGenerated = true; + console.log(` Generated initial context from existing memory`); + } + } + } + } catch (error) { + // Worker not running during install — non-critical + logger.debug('WINDSURF', 'Worker not running during install', {}, error as Error); + } + + if (!contextGenerated) { + // Create placeholder context file + const rulesDir = path.join(workspaceRoot, '.windsurf', 'rules'); + mkdirSync(rulesDir, { recursive: true }); + const rulesFile = path.join(rulesDir, 'claude-mem-context.md'); + const placeholderContent = `# Memory Context from Past Sessions + +*No context yet. Complete your first session and context will appear here.* + +Use claude-mem's MCP search tools for manual memory queries. +`; + writeFileSync(rulesFile, placeholderContent); + console.log(` Created placeholder context file (will populate after first session)`); + } + + // Register project for automatic context updates after summaries + registerWindsurfProject(projectName, workspaceRoot); + console.log(` Registered for auto-context updates`); +} + +/** + * Uninstall Windsurf hooks — removes claude-mem entries from hooks.json + */ +export function uninstallWindsurfHooks(): number { + console.log('\nUninstalling Claude-Mem Windsurf hooks...\n'); + + try { + // Remove our entries from hooks.json (preserve other integrations) + if (existsSync(WINDSURF_HOOKS_JSON_PATH)) { + try { + const config: WindsurfHooksJson = JSON.parse(readFileSync(WINDSURF_HOOKS_JSON_PATH, 'utf-8')); + + for (const eventName of WINDSURF_HOOK_EVENTS) { + if (config.hooks[eventName]) { + config.hooks[eventName] = config.hooks[eventName].filter( + (hook) => !hook.command.includes('worker-service') || !hook.command.includes('windsurf') + ); + // Remove empty arrays + if (config.hooks[eventName].length === 0) { + delete config.hooks[eventName]; + } + } + } + + // If no hooks remain, remove the file entirely + if (Object.keys(config.hooks).length === 0) { + unlinkSync(WINDSURF_HOOKS_JSON_PATH); + console.log(` Removed hooks.json (no hooks remaining)`); + } else { + writeFileSync(WINDSURF_HOOKS_JSON_PATH, JSON.stringify(config, null, 2)); + console.log(` Removed claude-mem entries from hooks.json (other hooks preserved)`); + } + } catch (error) { + // Corrupt file — just remove it + unlinkSync(WINDSURF_HOOKS_JSON_PATH); + console.log(` Removed corrupt hooks.json`); + } + } else { + console.log(` No hooks.json found`); + } + + // Remove context file from the current workspace + const workspaceRoot = process.cwd(); + const contextFile = path.join(workspaceRoot, '.windsurf', 'rules', 'claude-mem-context.md'); + if (existsSync(contextFile)) { + unlinkSync(contextFile); + console.log(` Removed context file`); + } + + // Unregister project + const projectName = path.basename(workspaceRoot); + unregisterWindsurfProject(projectName); + console.log(` Unregistered from auto-context updates`); + + console.log(`\nUninstallation complete!\n`); + console.log('Restart Windsurf to apply changes.'); + + return 0; + } catch (error) { + console.error(`\nUninstallation failed: ${(error as Error).message}`); + return 1; + } +} + +/** + * Check Windsurf hooks installation status + */ +export function checkWindsurfHooksStatus(): number { + console.log('\nClaude-Mem Windsurf Hooks Status\n'); + + if (existsSync(WINDSURF_HOOKS_JSON_PATH)) { + console.log(`User-level: Installed`); + console.log(` Config: ${WINDSURF_HOOKS_JSON_PATH}`); + + try { + const config: WindsurfHooksJson = JSON.parse(readFileSync(WINDSURF_HOOKS_JSON_PATH, 'utf-8')); + const registeredEvents = WINDSURF_HOOK_EVENTS.filter( + (event) => config.hooks[event]?.some( + (hook) => hook.command.includes('worker-service') && hook.command.includes('windsurf') + ) + ); + console.log(` Events: ${registeredEvents.length}/${WINDSURF_HOOK_EVENTS.length} registered`); + for (const event of registeredEvents) { + console.log(` - ${event}`); + } + } catch { + console.log(` Mode: Unable to parse hooks.json`); + } + + // Check for context file in current workspace + const contextFile = path.join(process.cwd(), '.windsurf', 'rules', 'claude-mem-context.md'); + if (existsSync(contextFile)) { + console.log(` Context: Active (current workspace)`); + } else { + console.log(` Context: Not yet generated for this workspace`); + } + } else { + console.log(`User-level: Not installed`); + console.log(`\nNo hooks installed. Run: claude-mem windsurf install\n`); + } + + console.log(''); + return 0; +} + +/** + * Handle windsurf subcommand for hooks installation + */ +export async function handleWindsurfCommand(subcommand: string, _args: string[]): Promise { + switch (subcommand) { + case 'install': + return installWindsurfHooks(); + + case 'uninstall': + return uninstallWindsurfHooks(); + + case 'status': + return checkWindsurfHooksStatus(); + + default: { + console.log(` +Claude-Mem Windsurf Integration + +Usage: claude-mem windsurf + +Commands: + install Install Windsurf hooks (user-level, ~/.codeium/windsurf/hooks.json) + uninstall Remove Windsurf hooks + status Check installation status + +Examples: + claude-mem windsurf install # Install hooks globally + claude-mem windsurf uninstall # Remove hooks + claude-mem windsurf status # Check if hooks are installed + +For more info: https://docs.claude-mem.ai/windsurf + `); + return 0; + } + } +} diff --git a/src/services/integrations/index.ts b/src/services/integrations/index.ts index db0abfa14..a985957de 100644 --- a/src/services/integrations/index.ts +++ b/src/services/integrations/index.ts @@ -1,6 +1,12 @@ /** - * Integrations module - IDE integrations (Cursor, etc.) + * Integrations module - IDE integrations (Cursor, Gemini CLI, OpenCode, Windsurf, etc.) */ export * from './types.js'; export * from './CursorHooksInstaller.js'; +export * from './GeminiCliHooksInstaller.js'; +export * from './OpenCodeInstaller.js'; +export * from './WindsurfHooksInstaller.js'; +export * from './OpenClawInstaller.js'; +export * from './CodexCliInstaller.js'; +export * from './McpIntegrations.js'; diff --git a/src/services/transcripts/config.ts b/src/services/transcripts/config.ts index 7390e7ff8..78c8e82db 100644 --- a/src/services/transcripts/config.ts +++ b/src/services/transcripts/config.ts @@ -8,7 +8,7 @@ export const DEFAULT_STATE_PATH = join(homedir(), '.claude-mem', 'transcript-wat const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { name: 'codex', - version: '0.2', + version: '0.3', description: 'Schema for Codex session JSONL files under ~/.codex/sessions.', events: [ { @@ -46,13 +46,14 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { }, { name: 'tool-use', - match: { path: 'payload.type', in: ['function_call', 'custom_tool_call', 'web_search_call'] }, + match: { path: 'payload.type', in: ['function_call', 'custom_tool_call', 'web_search_call', 'exec_command'] }, action: 'tool_use', fields: { toolId: 'payload.call_id', toolName: { coalesce: [ 'payload.name', + 'payload.type', { value: 'web_search' } ] }, @@ -60,6 +61,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { coalesce: [ 'payload.arguments', 'payload.input', + 'payload.command', 'payload.action' ] } @@ -67,7 +69,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { }, { name: 'tool-result', - match: { path: 'payload.type', in: ['function_call_output', 'custom_tool_call_output'] }, + match: { path: 'payload.type', in: ['function_call_output', 'custom_tool_call_output', 'exec_command_output'] }, action: 'tool_result', fields: { toolId: 'payload.call_id', @@ -76,7 +78,7 @@ const CODEX_SAMPLE_SCHEMA: TranscriptSchema = { }, { name: 'session-end', - match: { path: 'payload.type', equals: 'turn_aborted' }, + match: { path: 'payload.type', in: ['turn_aborted', 'turn_completed'] }, action: 'session_end' } ]