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/README.md +43 -3
- package/README.zh-CN.md +34 -0
- package/dist/{alias-DwnTg_u5.mjs → alias-hvk8y5gC.mjs} +280 -38
- package/dist/alias-hvk8y5gC.mjs.map +1 -0
- package/dist/cli.mjs +486 -25
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/alias-DwnTg_u5.mjs.map +0 -1
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-
|
|
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("--
|
|
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 || !!
|
|
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 && !
|
|
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);
|