droid-patch 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-DwnTg_u5.mjs";
2
+ import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-hvk8y5gC.mjs";
3
3
  import bin from "tiny-bin";
4
4
  import { styleText } from "node:util";
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
- import { homedir } from "node:os";
7
+ import { homedir, platform } from "node:os";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { execSync } from "node:child_process";
10
10
  import { chmod, mkdir, writeFile } from "node:fs/promises";
11
+ import { createInterface } from "node:readline/promises";
11
12
 
12
13
  //#region src/websearch-external.ts
13
14
  /**
@@ -518,8 +519,9 @@ process.on('SIGINT', function() { server.close(); process.exit(0); });
518
519
 
519
520
  //#endregion
520
521
  //#region src/websearch-patch.ts
522
+ const IS_WINDOWS$1 = platform() === "win32";
521
523
  /**
522
- * Generate unified wrapper script
524
+ * Generate unified wrapper script (Unix bash)
523
525
  */
524
526
  function generateUnifiedWrapper(droidPath, proxyScriptPath, standalone = false) {
525
527
  const standaloneEnv = standalone ? "STANDALONE_MODE=1 " : "";
@@ -601,6 +603,91 @@ exit $DROID_EXIT_CODE
601
603
  `;
602
604
  }
603
605
  /**
606
+ * Generate Windows wrapper script (.cmd batch file)
607
+ */
608
+ function generateWindowsWrapper(droidPath, proxyScriptPath, standalone = false) {
609
+ return `@echo off
610
+ setlocal enabledelayedexpansion
611
+ REM Droid with WebSearch
612
+ REM Auto-generated by droid-patch --websearch
613
+
614
+ set "PROXY_SCRIPT=${proxyScriptPath}"
615
+ set "DROID_BIN=${droidPath}"
616
+ set "PORT_FILE=%TEMP%\\droid-websearch-%RANDOM%%RANDOM%.port"
617
+
618
+ REM Check for passthrough commands
619
+ set "PASSTHROUGH=0"
620
+ for %%a in (%*) do (
621
+ if "%%a"=="--help" set "PASSTHROUGH=1"
622
+ if "%%a"=="-h" set "PASSTHROUGH=1"
623
+ if "%%a"=="--version" set "PASSTHROUGH=1"
624
+ if "%%a"=="-V" set "PASSTHROUGH=1"
625
+ if "%%a"=="help" set "PASSTHROUGH=1"
626
+ if "%%a"=="version" set "PASSTHROUGH=1"
627
+ if "%%a"=="completion" set "PASSTHROUGH=1"
628
+ if "%%a"=="completions" set "PASSTHROUGH=1"
629
+ if "%%a"=="exec" set "PASSTHROUGH=1"
630
+ if "%%a"=="plugin" set "PASSTHROUGH=1"
631
+ )
632
+ if "%PASSTHROUGH%"=="1" (
633
+ "%DROID_BIN%" %*
634
+ exit /b %ERRORLEVEL%
635
+ )
636
+
637
+ REM Start proxy server
638
+ ${standalone ? "set STANDALONE_MODE=1\r\n" : ""}set "SEARCH_PROXY_PORT=0"
639
+ set "SEARCH_PROXY_PORT_FILE=%PORT_FILE%"
640
+
641
+ if defined DROID_SEARCH_DEBUG (
642
+ echo [websearch] Starting proxy... 1>&2
643
+ start /b node "%PROXY_SCRIPT%"
644
+ ) else (
645
+ start /b node "%PROXY_SCRIPT%" >nul 2>&1
646
+ )
647
+
648
+ REM Wait for port file
649
+ set "RETRY=0"
650
+ :wait_loop
651
+ if %RETRY% GEQ 50 goto :proxy_failed
652
+ set /a RETRY+=1
653
+ timeout /t 1 /nobreak >nul 2>&1
654
+ if not exist "%PORT_FILE%" goto :wait_loop
655
+
656
+ REM Read port and check health
657
+ set /p ACTUAL_PORT=<"%PORT_FILE%"
658
+ if not defined ACTUAL_PORT goto :wait_loop
659
+
660
+ REM Simple health check using curl or PowerShell
661
+ where curl >nul 2>&1
662
+ if %ERRORLEVEL%==0 (
663
+ curl -s "http://127.0.0.1:%ACTUAL_PORT%/health" >nul 2>&1
664
+ if %ERRORLEVEL%==0 goto :proxy_ready
665
+ ) else (
666
+ powershell -Command "try { Invoke-WebRequest -Uri 'http://127.0.0.1:%ACTUAL_PORT%/health' -UseBasicParsing -TimeoutSec 2 | Out-Null; exit 0 } catch { exit 1 }" >nul 2>&1
667
+ if %ERRORLEVEL%==0 goto :proxy_ready
668
+ )
669
+ goto :wait_loop
670
+
671
+ :proxy_failed
672
+ echo [websearch] Failed to start proxy, running without websearch 1>&2
673
+ del "%PORT_FILE%" 2>nul
674
+ "%DROID_BIN%" %*
675
+ exit /b %ERRORLEVEL%
676
+
677
+ :proxy_ready
678
+ if defined DROID_SEARCH_DEBUG echo [websearch] Proxy ready on port %ACTUAL_PORT% 1>&2
679
+ del "%PORT_FILE%" 2>nul
680
+
681
+ set "FACTORY_API_BASE_URL_OVERRIDE=http://127.0.0.1:%ACTUAL_PORT%"
682
+ "%DROID_BIN%" %*
683
+ set "DROID_EXIT_CODE=%ERRORLEVEL%"
684
+
685
+ REM Cleanup: kill node processes started by this script
686
+ REM Note: Windows doesn't have easy process tree tracking, proxy will exit when parent exits
687
+ exit /b %DROID_EXIT_CODE%
688
+ `;
689
+ }
690
+ /**
604
691
  * Create unified WebSearch files
605
692
  *
606
693
  * @param outputDir - Directory to write files to
@@ -613,13 +700,13 @@ exit $DROID_EXIT_CODE
613
700
  async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase, standalone = false, useNativeProvider = false) {
614
701
  if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
615
702
  const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);
616
- const wrapperScriptPath = join(outputDir, aliasName);
703
+ const wrapperScriptPath = IS_WINDOWS$1 ? join(outputDir, `${aliasName}.cmd`) : join(outputDir, aliasName);
617
704
  const factoryApiUrl = apiBase || "https://api.factory.ai";
618
705
  await writeFile(proxyScriptPath, useNativeProvider ? generateNativeSearchProxyServer(factoryApiUrl) : generateExternalSearchProxyServer(factoryApiUrl));
619
706
  console.log(`[*] Created proxy script: ${proxyScriptPath}`);
620
707
  console.log(`[*] Mode: ${useNativeProvider ? "native provider (requires proxy plugin)" : "external providers"}`);
621
- await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath, standalone));
622
- await chmod(wrapperScriptPath, 493);
708
+ await writeFile(wrapperScriptPath, IS_WINDOWS$1 ? generateWindowsWrapper(droidPath, proxyScriptPath, standalone) : generateUnifiedWrapper(droidPath, proxyScriptPath, standalone));
709
+ if (!IS_WINDOWS$1) await chmod(wrapperScriptPath, 493);
623
710
  console.log(`[*] Created wrapper: ${wrapperScriptPath}`);
624
711
  if (standalone) console.log(`[*] Standalone mode enabled`);
625
712
  return {
@@ -628,9 +715,278 @@ async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiB
628
715
  };
629
716
  }
630
717
 
718
+ //#endregion
719
+ //#region src/model-manager.ts
720
+ /**
721
+ * Custom Model Manager
722
+ * Manages custom models in ~/.factory/settings.json
723
+ */
724
+ const FACTORY_DIR = join(homedir(), ".factory");
725
+ const SETTINGS_PATH = join(FACTORY_DIR, "settings.json");
726
+ /**
727
+ * Generate model ID from displayName and index
728
+ * Format: custom:{DisplayName}-{index} where spaces are replaced with -
729
+ * This matches droid's buildCustomModelId function
730
+ */
731
+ function generateModelId(displayName, index) {
732
+ return `custom:${displayName.trim().replace(/\s+/g, "-")}-${index}`;
733
+ }
734
+ /**
735
+ * Load settings.json
736
+ */
737
+ function loadSettings() {
738
+ if (!existsSync(SETTINGS_PATH)) return {};
739
+ try {
740
+ const content = readFileSync(SETTINGS_PATH, "utf-8");
741
+ return JSON.parse(content);
742
+ } catch {
743
+ return {};
744
+ }
745
+ }
746
+ /**
747
+ * Save settings.json
748
+ */
749
+ function saveSettings(settings) {
750
+ if (!existsSync(FACTORY_DIR)) mkdirSync(FACTORY_DIR, { recursive: true });
751
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
752
+ }
753
+ /**
754
+ * Rebuild all model IDs and indexes based on their array position
755
+ * This is necessary because droid uses array index as part of the ID
756
+ */
757
+ function rebuildModelIds(models) {
758
+ return models.map((model, index) => ({
759
+ ...model,
760
+ id: generateModelId(model.displayName, index),
761
+ index,
762
+ noImageSupport: model.noImageSupport ?? false
763
+ }));
764
+ }
765
+ /**
766
+ * Add a custom model at specified index (or end if not specified)
767
+ */
768
+ function addModel(modelName, displayName, baseUrl, apiKey, provider, insertIndex) {
769
+ const settings = loadSettings();
770
+ if (!settings.customModels) settings.customModels = [];
771
+ const actualIndex = insertIndex ?? settings.customModels.length;
772
+ if (actualIndex < 0 || actualIndex > settings.customModels.length) return {
773
+ success: false,
774
+ model: {},
775
+ message: `Invalid index ${actualIndex}. Valid range: 0-${settings.customModels.length}`
776
+ };
777
+ const newModel = {
778
+ model: modelName,
779
+ id: "",
780
+ baseUrl,
781
+ apiKey,
782
+ displayName,
783
+ provider,
784
+ index: 0,
785
+ noImageSupport: false
786
+ };
787
+ settings.customModels.splice(actualIndex, 0, newModel);
788
+ settings.customModels = rebuildModelIds(settings.customModels);
789
+ const insertedModel = settings.customModels[actualIndex];
790
+ saveSettings(settings);
791
+ return {
792
+ success: true,
793
+ model: insertedModel,
794
+ message: `Added model "${displayName}" at index ${actualIndex} with ID "${insertedModel.id}"`
795
+ };
796
+ }
797
+ /**
798
+ * Remove a custom model by index, ID, or displayName
799
+ */
800
+ function removeModel(identifier) {
801
+ const settings = loadSettings();
802
+ if (!settings.customModels || settings.customModels.length === 0) return {
803
+ success: false,
804
+ message: "No custom models configured"
805
+ };
806
+ let index;
807
+ const numericIndex = parseInt(identifier, 10);
808
+ if (!isNaN(numericIndex) && numericIndex >= 0 && numericIndex < settings.customModels.length) index = numericIndex;
809
+ else index = settings.customModels.findIndex((m) => m.id === identifier || m.displayName === identifier);
810
+ if (index === -1) return {
811
+ success: false,
812
+ message: `Model "${identifier}" not found. Use index (0-${settings.customModels.length - 1}), ID, or display name.`
813
+ };
814
+ const oldIds = settings.customModels.map((m) => ({
815
+ id: m.id,
816
+ displayName: m.displayName
817
+ }));
818
+ const removed = settings.customModels.splice(index, 1)[0];
819
+ const oldDefaultId = settings.sessionDefaultSettings?.model;
820
+ settings.customModels = rebuildModelIds(settings.customModels);
821
+ const updatedModels = [];
822
+ for (let i = index; i < settings.customModels.length; i++) {
823
+ const oldInfo = oldIds[i + 1];
824
+ const newModel = settings.customModels[i];
825
+ if (oldInfo && oldInfo.id !== newModel.id) updatedModels.push({
826
+ oldId: oldInfo.id,
827
+ newId: newModel.id,
828
+ displayName: newModel.displayName
829
+ });
830
+ }
831
+ if (oldDefaultId) if (oldDefaultId === removed.id) delete settings.sessionDefaultSettings.model;
832
+ else {
833
+ const updatedDefault = updatedModels.find((u) => u.oldId === oldDefaultId);
834
+ if (updatedDefault) settings.sessionDefaultSettings.model = updatedDefault.newId;
835
+ }
836
+ saveSettings(settings);
837
+ return {
838
+ success: true,
839
+ removed,
840
+ message: `Removed model "${removed.displayName}" (was at index ${index})`,
841
+ updatedModels
842
+ };
843
+ }
844
+ /**
845
+ * List all custom models
846
+ */
847
+ function listModels() {
848
+ return loadSettings().customModels || [];
849
+ }
850
+ /**
851
+ * Get current default model
852
+ */
853
+ function getDefaultModel() {
854
+ return loadSettings().sessionDefaultSettings?.model;
855
+ }
856
+ /**
857
+ * Print models list with detailed info
858
+ */
859
+ function printModelsList() {
860
+ const models = listModels();
861
+ const defaultModel = getDefaultModel();
862
+ console.log(styleText("cyan", "═".repeat(60)));
863
+ console.log(styleText(["cyan", "bold"], " Custom Models"));
864
+ console.log(styleText("cyan", "═".repeat(60)));
865
+ console.log();
866
+ if (models.length === 0) {
867
+ console.log(styleText("gray", " No custom models configured."));
868
+ console.log();
869
+ console.log(styleText("gray", " Add one with: npx droid-patch add-model"));
870
+ } else {
871
+ console.log(styleText("white", ` Found ${models.length} model(s):`));
872
+ console.log();
873
+ for (let i = 0; i < models.length; i++) {
874
+ const model = models[i];
875
+ const defaultMark = model.id === defaultModel ? styleText("green", " [DEFAULT]") : "";
876
+ const indexMark = styleText("gray", `[${i}]`);
877
+ console.log(` ${indexMark} ${styleText(["cyan", "bold"], model.displayName)}${defaultMark}`);
878
+ console.log(styleText("gray", ` ID: ${model.id}`));
879
+ console.log(styleText("gray", ` Model: ${model.model}`));
880
+ console.log(styleText("gray", ` Provider: ${model.provider}`));
881
+ console.log(styleText("gray", ` Base URL: ${model.baseUrl}`));
882
+ console.log(styleText("gray", ` API Key: ${model.apiKey.substring(0, 8)}...`));
883
+ console.log();
884
+ }
885
+ }
886
+ console.log(styleText("gray", ` Settings file: ${SETTINGS_PATH}`));
887
+ console.log();
888
+ }
889
+ /**
890
+ * Interactive prompt helper using readline/promises
891
+ */
892
+ async function prompt(question, defaultValue) {
893
+ const defaultHint = defaultValue ? ` (${defaultValue})` : "";
894
+ const rl = createInterface({
895
+ input: process.stdin,
896
+ output: process.stdout
897
+ });
898
+ try {
899
+ return (await rl.question(`${question}${defaultHint}: `)).trim() || defaultValue || "";
900
+ } finally {
901
+ rl.close();
902
+ }
903
+ }
904
+ /**
905
+ * Interactive prompt for selecting from options
906
+ */
907
+ async function promptSelect(question, options) {
908
+ console.log(question);
909
+ options.forEach((opt, i) => {
910
+ console.log(styleText("cyan", ` ${i + 1}. ${opt}`));
911
+ });
912
+ const rl = createInterface({
913
+ input: process.stdin,
914
+ output: process.stdout
915
+ });
916
+ try {
917
+ const answer = await rl.question("Select (number): ");
918
+ const idx = parseInt(answer.trim(), 10) - 1;
919
+ if (idx >= 0 && idx < options.length) return options[idx];
920
+ return options[0];
921
+ } finally {
922
+ rl.close();
923
+ }
924
+ }
925
+ /**
926
+ * Interactive mode for adding a model
927
+ */
928
+ async function addModelInteractive(insertIndex) {
929
+ console.log(styleText("cyan", "═".repeat(60)));
930
+ console.log(styleText(["cyan", "bold"], " Add Custom Model (Interactive)"));
931
+ console.log(styleText("cyan", "═".repeat(60)));
932
+ console.log();
933
+ const models = listModels();
934
+ if (models.length > 0) {
935
+ console.log(styleText("gray", `Current models: ${models.length}`));
936
+ models.forEach((m, i) => {
937
+ console.log(styleText("gray", ` [${i}] ${m.displayName}`));
938
+ });
939
+ console.log();
940
+ }
941
+ const displayName = await prompt("Display name (e.g., 'Opus [proxy]')");
942
+ if (!displayName) {
943
+ console.log(styleText("red", "Display name is required"));
944
+ return;
945
+ }
946
+ const modelName = await prompt("Model name (e.g., 'claude-sonnet-4-20250514')");
947
+ if (!modelName) {
948
+ console.log(styleText("red", "Model name is required"));
949
+ return;
950
+ }
951
+ const baseUrl = await prompt("Base URL (e.g., 'http://127.0.0.1:20002/droid')");
952
+ if (!baseUrl) {
953
+ console.log(styleText("red", "Base URL is required"));
954
+ return;
955
+ }
956
+ const apiKey = await prompt("API Key");
957
+ if (!apiKey) {
958
+ console.log(styleText("red", "API Key is required"));
959
+ return;
960
+ }
961
+ const provider = await promptSelect("Provider:", [
962
+ "anthropic",
963
+ "openai",
964
+ "generic-chat-completion-api"
965
+ ]);
966
+ let actualIndex = insertIndex;
967
+ if (actualIndex === void 0 && models.length > 0) {
968
+ const indexStr = await prompt(`Insert at index (0-${models.length})`, String(models.length));
969
+ actualIndex = parseInt(indexStr, 10);
970
+ if (isNaN(actualIndex) || actualIndex < 0 || actualIndex > models.length) actualIndex = models.length;
971
+ }
972
+ console.log();
973
+ const result = addModel(modelName, displayName, baseUrl, apiKey, provider, actualIndex);
974
+ if (result.success) {
975
+ console.log(styleText("green", `[+] ${result.message}`));
976
+ console.log();
977
+ console.log(styleText("white", "Model details:"));
978
+ console.log(styleText("gray", ` ID: ${result.model.id}`));
979
+ console.log(styleText("gray", ` Display Name: ${result.model.displayName}`));
980
+ console.log(styleText("gray", ` Model: ${result.model.model}`));
981
+ console.log(styleText("gray", ` Provider: ${result.model.provider}`));
982
+ console.log(styleText("gray", ` Base URL: ${result.model.baseUrl}`));
983
+ } else console.log(styleText("red", `Error: ${result.message}`));
984
+ }
985
+
631
986
  //#endregion
632
987
  //#region src/cli.ts
633
988
  const __dirname = dirname(fileURLToPath(import.meta.url));
989
+ const IS_WINDOWS = platform() === "win32";
634
990
  function getVersion() {
635
991
  try {
636
992
  const pkgPath = join(__dirname, "..", "package.json");
@@ -659,6 +1015,27 @@ function getDroidVersion(droidPath) {
659
1015
  }
660
1016
  function findDefaultDroidPath() {
661
1017
  const home = homedir();
1018
+ if (IS_WINDOWS) {
1019
+ try {
1020
+ const firstResult = execSync("where droid", {
1021
+ encoding: "utf-8",
1022
+ stdio: [
1023
+ "pipe",
1024
+ "pipe",
1025
+ "pipe"
1026
+ ]
1027
+ }).trim().split(/\r?\n/)[0];
1028
+ if (firstResult && existsSync(firstResult)) return firstResult;
1029
+ } catch {}
1030
+ const windowsPaths = [
1031
+ join(home, ".droid", "bin", "droid.exe"),
1032
+ join(home, "AppData", "Local", "Programs", "droid", "droid.exe"),
1033
+ join(home, "scoop", "apps", "droid", "current", "droid.exe"),
1034
+ "./droid.exe"
1035
+ ];
1036
+ for (const p of windowsPaths) if (existsSync(p)) return p;
1037
+ return join(home, ".droid", "bin", "droid.exe");
1038
+ }
662
1039
  try {
663
1040
  const result = execSync("which droid", {
664
1041
  encoding: "utf-8",
@@ -680,7 +1057,7 @@ function findDefaultDroidPath() {
680
1057
  for (const p of paths) if (existsSync(p)) return p;
681
1058
  return join(home, ".droid", "bin", "droid");
682
1059
  }
683
- bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy with external providers (Smithery, Google PSE, etc.)").option("--websearch-proxy", "Enable native provider websearch (requires proxy plugin, reads ~/.factory/settings.json)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--auto-high", "Set default autonomy mode to auto-high (bypass settings.json race condition)").option("--spec-model-custom", "Enable custom models as spec model (show in UI selector + bypass validation)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
1060
+ bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy with external providers (Smithery, Google PSE, etc.)").option("--websearch-proxy", "Enable native provider websearch (requires proxy plugin, reads ~/.factory/settings.json)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--spec-model-custom", "Enable custom models as spec model (show in UI selector + bypass validation)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
684
1061
  const alias = args?.[0];
685
1062
  const isCustom = options["is-custom"];
686
1063
  const skipLogin = options["skip-login"];
@@ -691,7 +1068,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
691
1068
  const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
692
1069
  const reasoningEffort = options["reasoning-effort"];
693
1070
  const noTelemetry = options["disable-telemetry"];
694
- const autoHigh = options["auto-high"];
695
1071
  const specModelCustom = options["spec-model-custom"];
696
1072
  const dryRun = options["dry-run"];
697
1073
  const path = options.path || findDefaultDroidPath();
@@ -699,7 +1075,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
699
1075
  const backup = options.backup !== false;
700
1076
  const verbose = options.verbose;
701
1077
  const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
702
- const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!autoHigh || !!specModelCustom || !!apiBase && !websearch && !websearchProxy;
1078
+ const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!specModelCustom || !!apiBase && !websearch && !websearchProxy;
703
1079
  if (websearch && websearchProxy) {
704
1080
  console.log(styleText("red", "Error: Cannot use --websearch and --websearch-proxy together"));
705
1081
  console.log(styleText("gray", "Choose one:"));
@@ -788,7 +1164,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
788
1164
  }
789
1165
  return;
790
1166
  }
791
- if (!isCustom && !skipLogin && !apiBase && !websearch && !reasoningEffort && !noTelemetry && !autoHigh && !specModelCustom) {
1167
+ if (!isCustom && !skipLogin && !apiBase && !websearch && !reasoningEffort && !noTelemetry && !specModelCustom) {
792
1168
  console.log(styleText("yellow", "No patch flags specified. Available patches:"));
793
1169
  console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
794
1170
  console.log(styleText("gray", " --skip-login Bypass login by injecting a fake API key"));
@@ -796,7 +1172,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
796
1172
  console.log(styleText("gray", " --websearch Enable local WebSearch proxy"));
797
1173
  console.log(styleText("gray", " --reasoning-effort Set reasoning effort level for custom models"));
798
1174
  console.log(styleText("gray", " --disable-telemetry Disable telemetry and Sentry error reporting"));
799
- console.log(styleText("gray", " --auto-high Set default autonomy mode to auto-high"));
800
1175
  console.log(styleText("gray", " --spec-model-custom Enable custom models as spec model"));
801
1176
  console.log(styleText("gray", " --standalone Standalone mode: mock non-LLM Factory APIs"));
802
1177
  console.log();
@@ -909,12 +1284,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
909
1284
  replacement: Buffer.from("!0||this.webEvents.length")
910
1285
  });
911
1286
  }
912
- if (autoHigh) patches.push({
913
- name: "autoHighAutonomy",
914
- description: "Hardcode getCurrentAutonomyMode() to return \"auto-high\"",
915
- pattern: Buffer.from("getCurrentAutonomyMode(){return this.autonomyMode}"),
916
- replacement: Buffer.from("getCurrentAutonomyMode(){return\"auto-high\" }")
917
- });
918
1287
  if (specModelCustom) {
919
1288
  patches.push({
920
1289
  name: "specModelCustomUIShow",
@@ -991,7 +1360,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
991
1360
  reasoningEffort: !!reasoningEffort,
992
1361
  noTelemetry: !!noTelemetry,
993
1362
  standalone: !!standalone,
994
- autoHigh: !!autoHigh,
995
1363
  specModelCustom: !!specModelCustom
996
1364
  }, {
997
1365
  droidPatchVersion: version,
@@ -1189,12 +1557,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1189
1557
  replacement: Buffer.from("!0||this.webEvents.length")
1190
1558
  });
1191
1559
  }
1192
- if (meta.patches.autoHigh) patches.push({
1193
- name: "autoHighAutonomy",
1194
- description: "Hardcode getCurrentAutonomyMode() to return \"auto-high\"",
1195
- pattern: Buffer.from("getCurrentAutonomyMode(){return this.autonomyMode}"),
1196
- replacement: Buffer.from("getCurrentAutonomyMode(){return\"auto-high\" }")
1197
- });
1198
1560
  if (meta.patches.specModelCustom) {
1199
1561
  patches.push({
1200
1562
  name: "specModelCustomUIShow",
@@ -1318,6 +1680,105 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1318
1680
  console.log(styleText("gray", ` Success: ${successCount}, Failed: ${failCount}`));
1319
1681
  }
1320
1682
  console.log(styleText("cyan", "═".repeat(60)));
1683
+ }).command("add-model", "Add a custom model to settings.json (interactive if no options)").option("-m, --model <model>", "Model name (e.g., claude-sonnet-4-20250514)").option("-n, --name <name>", "Display name (e.g., 'Opus [proxy]')").option("-u, --url <url>", "Base URL (e.g., http://127.0.0.1:20002/droid)").option("-k, --key <key>", "API key").option("-p, --provider <provider>", "Provider: anthropic, openai, or generic-chat-completion-api").option("-i, --index <index>", "Insert at index (auto-assigned if not specified)").action(async (options) => {
1684
+ const model = options.model;
1685
+ const displayName = options.name;
1686
+ const baseUrl = options.url;
1687
+ const apiKey = options.key;
1688
+ const providerStr = options.provider;
1689
+ const indexStr = options.index;
1690
+ if (!model && !displayName && !baseUrl && !apiKey && !providerStr) {
1691
+ await addModelInteractive(indexStr ? parseInt(indexStr, 10) : void 0);
1692
+ return;
1693
+ }
1694
+ if (!model || !displayName || !baseUrl || !apiKey || !providerStr) {
1695
+ console.log(styleText("yellow", "Missing required options. Enter interactive mode or provide all options."));
1696
+ console.log();
1697
+ console.log(styleText("white", "Interactive mode:"));
1698
+ console.log(styleText("cyan", " npx droid-patch add-model"));
1699
+ console.log();
1700
+ console.log(styleText("white", "Full command mode:"));
1701
+ console.log(styleText("cyan", " npx droid-patch add-model \\"));
1702
+ console.log(styleText("cyan", " -m \"claude-sonnet-4-20250514\" \\"));
1703
+ console.log(styleText("cyan", " -n \"Opus [proxy]\" \\"));
1704
+ console.log(styleText("cyan", " -u \"http://127.0.0.1:20002/droid\" \\"));
1705
+ console.log(styleText("cyan", " -k \"your-api-key\" \\"));
1706
+ console.log(styleText("cyan", " -p \"anthropic\""));
1707
+ console.log();
1708
+ console.log(styleText("gray", "Providers: anthropic, openai, generic-chat-completion-api"));
1709
+ console.log(styleText("gray", "Optional: -i <index> to insert at specific position"));
1710
+ process.exit(1);
1711
+ }
1712
+ const validProviders = [
1713
+ "anthropic",
1714
+ "openai",
1715
+ "generic-chat-completion-api"
1716
+ ];
1717
+ if (!validProviders.includes(providerStr)) {
1718
+ console.log(styleText("red", `Error: Invalid provider "${providerStr}"`));
1719
+ console.log(styleText("gray", `Valid providers: ${validProviders.join(", ")}`));
1720
+ process.exit(1);
1721
+ }
1722
+ const index = indexStr ? parseInt(indexStr, 10) : void 0;
1723
+ if (indexStr && (isNaN(index) || index < 0)) {
1724
+ console.log(styleText("red", "Error: Index must be a non-negative number"));
1725
+ process.exit(1);
1726
+ }
1727
+ const result = addModel(model, displayName, baseUrl, apiKey, providerStr, index);
1728
+ if (result.success) {
1729
+ console.log(styleText("green", `[+] ${result.message}`));
1730
+ console.log();
1731
+ console.log(styleText("white", "Model details:"));
1732
+ console.log(styleText("gray", ` ID: ${result.model.id}`));
1733
+ console.log(styleText("gray", ` Display Name: ${result.model.displayName}`));
1734
+ console.log(styleText("gray", ` Model: ${result.model.model}`));
1735
+ console.log(styleText("gray", ` Provider: ${result.model.provider}`));
1736
+ console.log(styleText("gray", ` Base URL: ${result.model.baseUrl}`));
1737
+ } else {
1738
+ console.log(styleText("red", `Error: ${result.message}`));
1739
+ process.exit(1);
1740
+ }
1741
+ }).command("remove-model", "Remove a custom model from settings.json").argument("<identifier>", "Model index, ID, or display name to remove").action((options, args) => {
1742
+ const identifier = args?.[0];
1743
+ if (!identifier) {
1744
+ console.log(styleText("red", "Error: Model identifier required"));
1745
+ console.log();
1746
+ console.log(styleText("white", "Usage:"));
1747
+ console.log(styleText("cyan", " npx droid-patch remove-model <identifier>"));
1748
+ console.log();
1749
+ console.log(styleText("gray", "Identifier can be:"));
1750
+ console.log(styleText("gray", " - Index number (e.g., 0, 1, 2)"));
1751
+ console.log(styleText("gray", " - Model ID (e.g., custom:Opus-[proxy]-0)"));
1752
+ console.log(styleText("gray", " - Display name (e.g., 'Opus [proxy]')"));
1753
+ console.log();
1754
+ console.log(styleText("gray", "Use 'npx droid-patch list-models' to see all models"));
1755
+ process.exit(1);
1756
+ }
1757
+ const result = removeModel(identifier);
1758
+ if (result.success && result.removed) {
1759
+ console.log(styleText("green", `[+] ${result.message}`));
1760
+ console.log();
1761
+ console.log(styleText("white", "Removed model details:"));
1762
+ console.log(styleText("gray", ` ID: ${result.removed.id}`));
1763
+ console.log(styleText("gray", ` Display Name: ${result.removed.displayName}`));
1764
+ console.log(styleText("gray", ` Model: ${result.removed.model}`));
1765
+ console.log(styleText("gray", ` Provider: ${result.removed.provider}`));
1766
+ if (result.updatedModels && result.updatedModels.length > 0) {
1767
+ console.log();
1768
+ console.log(styleText("yellow", "Updated model IDs (due to index shift):"));
1769
+ for (const updated of result.updatedModels) {
1770
+ console.log(styleText("gray", ` ${updated.displayName}:`));
1771
+ console.log(styleText("gray", ` ${updated.oldId} → ${updated.newId}`));
1772
+ }
1773
+ }
1774
+ console.log();
1775
+ console.log(styleText("gray", "Use 'npx droid-patch list-models' to see current state."));
1776
+ } else {
1777
+ console.log(styleText("red", `Error: ${result.message}`));
1778
+ process.exit(1);
1779
+ }
1780
+ }).command("list-models", "List all custom models in settings.json").action(() => {
1781
+ printModelsList();
1321
1782
  }).run().catch((err) => {
1322
1783
  console.error(err);
1323
1784
  process.exit(1);