gnosys 5.3.3 → 5.4.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.js +156 -30
- package/dist/cli.js.map +1 -1
- package/dist/lib/db.d.ts +6 -3
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +13 -8
- package/dist/lib/db.js.map +1 -1
- package/dist/lib/modelValidation.d.ts +22 -0
- package/dist/lib/modelValidation.d.ts.map +1 -0
- package/dist/lib/modelValidation.js +157 -0
- package/dist/lib/modelValidation.js.map +1 -0
- package/dist/lib/paths.d.ts +32 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +44 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/remoteWizard.d.ts +2 -1
- package/dist/lib/remoteWizard.d.ts.map +1 -1
- package/dist/lib/remoteWizard.js +6 -3
- package/dist/lib/remoteWizard.js.map +1 -1
- package/dist/lib/setup.d.ts +25 -0
- package/dist/lib/setup.d.ts.map +1 -1
- package/dist/lib/setup.js +459 -25
- package/dist/lib/setup.js.map +1 -1
- package/dist/postinstall.js +2 -2
- package/dist/postinstall.js.map +1 -1
- package/dist/sandbox/helper-template.d.ts.map +1 -1
- package/dist/sandbox/helper-template.js +8 -2
- package/dist/sandbox/helper-template.js.map +1 -1
- package/dist/sandbox/server.d.ts.map +1 -1
- package/dist/sandbox/server.js +2 -2
- package/dist/sandbox/server.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/setup.js
CHANGED
|
@@ -15,6 +15,8 @@ import path from "path";
|
|
|
15
15
|
import os from "os";
|
|
16
16
|
import { execSync } from "child_process";
|
|
17
17
|
import { loadConfig, updateConfig, getProviderModel, } from "./config.js";
|
|
18
|
+
import { validateModel } from "./modelValidation.js";
|
|
19
|
+
import { getGnosysHome } from "./paths.js";
|
|
18
20
|
// ─── ANSI Colors ────────────────────────────────────────────────────────────
|
|
19
21
|
const BOLD = "\x1b[1m";
|
|
20
22
|
const DIM = "\x1b[2m";
|
|
@@ -479,8 +481,76 @@ export async function detectIDEs(projectDir) {
|
|
|
479
481
|
// Not installed
|
|
480
482
|
}
|
|
481
483
|
}
|
|
484
|
+
// Check for Gemini CLI — CLI in PATH or global ~/.gemini/ directory
|
|
485
|
+
try {
|
|
486
|
+
execSync("which gemini", { stdio: "ignore" });
|
|
487
|
+
detected.push("gemini-cli");
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
try {
|
|
491
|
+
const stat = await fs.stat(path.join(home, ".gemini"));
|
|
492
|
+
if (stat.isDirectory())
|
|
493
|
+
detected.push("gemini-cli");
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// Not installed
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Check for Antigravity — ~/.gemini/antigravity/ directory or app installed
|
|
500
|
+
// (Antigravity stores its MCP config at ~/.gemini/antigravity/mcp_config.json)
|
|
501
|
+
try {
|
|
502
|
+
const stat = await fs.stat(path.join(home, ".gemini", "antigravity"));
|
|
503
|
+
if (stat.isDirectory())
|
|
504
|
+
detected.push("antigravity");
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Also check macOS Applications
|
|
508
|
+
try {
|
|
509
|
+
await fs.stat("/Applications/Antigravity.app");
|
|
510
|
+
detected.push("antigravity");
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
// Not installed
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Check for Claude Desktop — distinct from Claude Code CLI. Detected via the
|
|
517
|
+
// app bundle on macOS or the platform-specific config dir.
|
|
518
|
+
try {
|
|
519
|
+
const cfg = claudeDesktopConfigPath();
|
|
520
|
+
const cfgDir = path.dirname(cfg);
|
|
521
|
+
const stat = await fs.stat(cfgDir);
|
|
522
|
+
if (stat.isDirectory())
|
|
523
|
+
detected.push("claude-desktop");
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// Also check macOS Applications
|
|
527
|
+
try {
|
|
528
|
+
await fs.stat("/Applications/Claude.app");
|
|
529
|
+
detected.push("claude-desktop");
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// Not installed
|
|
533
|
+
}
|
|
534
|
+
}
|
|
482
535
|
return detected;
|
|
483
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Resolve the platform-specific Claude Desktop config file path.
|
|
539
|
+
* macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
540
|
+
* Windows: %APPDATA%/Claude/claude_desktop_config.json
|
|
541
|
+
* Linux: ~/.config/Claude/claude_desktop_config.json (no official build yet)
|
|
542
|
+
*/
|
|
543
|
+
function claudeDesktopConfigPath() {
|
|
544
|
+
const home = os.homedir();
|
|
545
|
+
if (process.platform === "darwin") {
|
|
546
|
+
return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
547
|
+
}
|
|
548
|
+
if (process.platform === "win32") {
|
|
549
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
550
|
+
return path.join(appData, "Claude", "claude_desktop_config.json");
|
|
551
|
+
}
|
|
552
|
+
return path.join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
553
|
+
}
|
|
484
554
|
/**
|
|
485
555
|
* Set up Gnosys MCP integration for a specific IDE.
|
|
486
556
|
*/
|
|
@@ -542,6 +612,74 @@ export async function setupIDE(ide, projectDir) {
|
|
|
542
612
|
}
|
|
543
613
|
return { success: true, message: "Codex config updated (.codex/config.toml)" };
|
|
544
614
|
}
|
|
615
|
+
case "gemini-cli": {
|
|
616
|
+
// Gemini CLI reads MCP servers from ~/.gemini/settings.json (user-level)
|
|
617
|
+
const geminiDir = path.join(os.homedir(), ".gemini");
|
|
618
|
+
const settingsPath = path.join(geminiDir, "settings.json");
|
|
619
|
+
await fs.mkdir(geminiDir, { recursive: true });
|
|
620
|
+
let config = {};
|
|
621
|
+
try {
|
|
622
|
+
const existing = await fs.readFile(settingsPath, "utf-8");
|
|
623
|
+
config = JSON.parse(existing);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
// File doesn't exist or is invalid — start fresh
|
|
627
|
+
}
|
|
628
|
+
const servers = (config.mcpServers ?? {});
|
|
629
|
+
servers.gnosys = { command: "gnosys", args: ["serve"] };
|
|
630
|
+
config.mcpServers = servers;
|
|
631
|
+
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
632
|
+
return { success: true, message: "Gemini CLI MCP config updated (~/.gemini/settings.json)" };
|
|
633
|
+
}
|
|
634
|
+
case "antigravity": {
|
|
635
|
+
// Antigravity reads MCP servers from ~/.gemini/antigravity/mcp_config.json
|
|
636
|
+
// (separate file from Gemini CLI's settings.json, even though they share the parent dir)
|
|
637
|
+
const antigravityDir = path.join(os.homedir(), ".gemini", "antigravity");
|
|
638
|
+
const configPath = path.join(antigravityDir, "mcp_config.json");
|
|
639
|
+
await fs.mkdir(antigravityDir, { recursive: true });
|
|
640
|
+
let config = {};
|
|
641
|
+
try {
|
|
642
|
+
const existing = await fs.readFile(configPath, "utf-8");
|
|
643
|
+
config = JSON.parse(existing);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// File doesn't exist or is invalid — start fresh
|
|
647
|
+
}
|
|
648
|
+
const servers = (config.mcpServers ?? {});
|
|
649
|
+
servers.gnosys = { command: "gnosys", args: ["serve"] };
|
|
650
|
+
config.mcpServers = servers;
|
|
651
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
652
|
+
return { success: true, message: "Antigravity MCP config updated (~/.gemini/antigravity/mcp_config.json)" };
|
|
653
|
+
}
|
|
654
|
+
case "claude-desktop": {
|
|
655
|
+
// Claude Desktop reads MCP servers from claude_desktop_config.json
|
|
656
|
+
// in a platform-specific app data directory. Distinct from Claude
|
|
657
|
+
// Code CLI which uses `claude mcp add`.
|
|
658
|
+
const configPath = claudeDesktopConfigPath();
|
|
659
|
+
const configDir = path.dirname(configPath);
|
|
660
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
661
|
+
let config = {};
|
|
662
|
+
try {
|
|
663
|
+
const existing = await fs.readFile(configPath, "utf-8");
|
|
664
|
+
config = JSON.parse(existing);
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
// File doesn't exist or is invalid — start fresh
|
|
668
|
+
}
|
|
669
|
+
const servers = (config.mcpServers ?? {});
|
|
670
|
+
servers.gnosys = { command: "gnosys", args: ["serve"] };
|
|
671
|
+
config.mcpServers = servers;
|
|
672
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
673
|
+
// Display path with ~ prefix when inside HOME for clarity
|
|
674
|
+
const home = os.homedir();
|
|
675
|
+
const displayPath = configPath.startsWith(home)
|
|
676
|
+
? configPath.replace(home, "~")
|
|
677
|
+
: configPath;
|
|
678
|
+
return {
|
|
679
|
+
success: true,
|
|
680
|
+
message: `Claude Desktop MCP config updated (${displayPath}). Restart Claude Desktop for the change to take effect.`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
545
683
|
default:
|
|
546
684
|
return { success: false, message: `Unknown IDE: ${ide}` };
|
|
547
685
|
}
|
|
@@ -680,7 +818,7 @@ async function loadExistingConfig(projectDir) {
|
|
|
680
818
|
}
|
|
681
819
|
// Try global config at ~/.gnosys
|
|
682
820
|
try {
|
|
683
|
-
const globalStore =
|
|
821
|
+
const globalStore = getGnosysHome();
|
|
684
822
|
const stat = await fs.stat(path.join(globalStore, "gnosys.json"));
|
|
685
823
|
if (stat.isFile()) {
|
|
686
824
|
return await loadConfig(globalStore);
|
|
@@ -714,12 +852,15 @@ async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
|
714
852
|
}
|
|
715
853
|
/**
|
|
716
854
|
* Let the user pick a model from a provider's tiers.
|
|
717
|
-
* Returns the model string.
|
|
855
|
+
* Returns the model string. Includes a "Custom (enter model name)"
|
|
856
|
+
* option so users can type any model ID not in the curated list.
|
|
718
857
|
*/
|
|
719
858
|
async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
720
859
|
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
721
|
-
if (!tiers || tiers.length === 0)
|
|
722
|
-
|
|
860
|
+
if (!tiers || tiers.length === 0) {
|
|
861
|
+
// No tiers available — fall back to direct entry
|
|
862
|
+
return await askInput(rl, "Model name");
|
|
863
|
+
}
|
|
723
864
|
const isLocal = provider === "ollama" || provider === "lmstudio";
|
|
724
865
|
const currentHint = currentModel ? ` ${DIM}(current: ${currentModel})${RESET}` : "";
|
|
725
866
|
const tierOptions = tiers.map((t) => {
|
|
@@ -729,7 +870,13 @@ async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
|
729
870
|
}
|
|
730
871
|
return `${t.name} (${t.model}) ${DIM}${formatPrice(t.input, t.output)}${RESET}${rec}`;
|
|
731
872
|
});
|
|
873
|
+
tierOptions.push(`Custom ${DIM}(enter model name)${RESET}`);
|
|
732
874
|
const tierIndex = await askChoice(rl, `${stepLabel}${currentHint}`, tierOptions);
|
|
875
|
+
// Custom option is the last entry
|
|
876
|
+
if (tierIndex === tiers.length) {
|
|
877
|
+
const custom = await askInput(rl, "Enter model name");
|
|
878
|
+
return custom;
|
|
879
|
+
}
|
|
733
880
|
return tiers[tierIndex].model;
|
|
734
881
|
}
|
|
735
882
|
// ─── Main Setup Wizard ──────────────────────────────────────────────────────
|
|
@@ -789,7 +936,8 @@ export async function runSetup(opts) {
|
|
|
789
936
|
? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
|
|
790
937
|
: undefined;
|
|
791
938
|
// ─── Pre-check: Upgrade detection ─────────────────────────────────
|
|
792
|
-
const
|
|
939
|
+
const { GnosysDB: GnosysDBForUpgrade } = await import("./db.js");
|
|
940
|
+
const centralDbPath = GnosysDBForUpgrade.getCentralDbPath();
|
|
793
941
|
const centralDbExists = fsSync.existsSync(centralDbPath);
|
|
794
942
|
if (centralDbExists) {
|
|
795
943
|
const projects = await getRegisteredProjects();
|
|
@@ -882,8 +1030,14 @@ export async function runSetup(opts) {
|
|
|
882
1030
|
}
|
|
883
1031
|
return `${t.name} (${t.model}) ${DIM}${formatPrice(t.input, t.output)}${RESET}${rec}`;
|
|
884
1032
|
});
|
|
1033
|
+
tierOptions.push(`Custom ${DIM}(enter model name)${RESET}`);
|
|
885
1034
|
const tierIndex = await askChoice(rl, `${BOLD}Step 2/5${RESET} ${DIM}\u2014${RESET} Choose model tier${currentModelHint}`, tierOptions);
|
|
886
|
-
|
|
1035
|
+
if (tierIndex === tiers.length) {
|
|
1036
|
+
model = await askInput(rl, "Enter model name");
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
model = tiers[tierIndex].model;
|
|
1040
|
+
}
|
|
887
1041
|
}
|
|
888
1042
|
}
|
|
889
1043
|
else if (provider === "custom") {
|
|
@@ -931,6 +1085,9 @@ export async function runSetup(opts) {
|
|
|
931
1085
|
// ─── Step 3/5 — API key ───────────────────────────────────────────
|
|
932
1086
|
let apiKeyWritten = false;
|
|
933
1087
|
let apiKeySource = "";
|
|
1088
|
+
// Captured key value (kept in memory for the validation step below).
|
|
1089
|
+
// Not persisted beyond the wizard run.
|
|
1090
|
+
let capturedApiKey = "";
|
|
934
1091
|
const needsKey = !isSkip &&
|
|
935
1092
|
provider !== "ollama" &&
|
|
936
1093
|
provider !== "lmstudio";
|
|
@@ -959,6 +1116,16 @@ export async function runSetup(opts) {
|
|
|
959
1116
|
console.log(` ${CHECK} Found existing key (${source})`);
|
|
960
1117
|
if (existingKey) {
|
|
961
1118
|
console.log(` ${DIM} ${maskKey(existingKey)}${RESET}`);
|
|
1119
|
+
capturedApiKey = existingKey;
|
|
1120
|
+
}
|
|
1121
|
+
else if (existingKeySource === "macOS Keychain" && process.platform === "darwin") {
|
|
1122
|
+
// Pull key out of keychain so we can validate
|
|
1123
|
+
try {
|
|
1124
|
+
capturedApiKey = execSync(`security find-generic-password -a "$USER" -s "${envVarName}" -w`, { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
1125
|
+
}
|
|
1126
|
+
catch {
|
|
1127
|
+
// Couldn't read it — validation will be skipped
|
|
1128
|
+
}
|
|
962
1129
|
}
|
|
963
1130
|
apiKeyWritten = true;
|
|
964
1131
|
apiKeySource = existingKeySource || "env";
|
|
@@ -971,6 +1138,7 @@ export async function runSetup(opts) {
|
|
|
971
1138
|
// Fall through to key storage options below
|
|
972
1139
|
apiKeyWritten = false;
|
|
973
1140
|
apiKeySource = "";
|
|
1141
|
+
capturedApiKey = "";
|
|
974
1142
|
}
|
|
975
1143
|
}
|
|
976
1144
|
if (!apiKeyWritten) {
|
|
@@ -1009,6 +1177,7 @@ export async function runSetup(opts) {
|
|
|
1009
1177
|
console.log(` ${CHECK} Key ${existingKey ? "moved" : "saved"} to macOS Keychain (${maskKey(key)})`);
|
|
1010
1178
|
apiKeyWritten = true;
|
|
1011
1179
|
apiKeySource = "macOS Keychain";
|
|
1180
|
+
capturedApiKey = key;
|
|
1012
1181
|
}
|
|
1013
1182
|
else {
|
|
1014
1183
|
console.log(` ${CROSS} Failed to write to Keychain. Falling back to .env file.`);
|
|
@@ -1016,6 +1185,7 @@ export async function runSetup(opts) {
|
|
|
1016
1185
|
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
|
|
1017
1186
|
apiKeyWritten = true;
|
|
1018
1187
|
apiKeySource = "~/.config/gnosys/.env";
|
|
1188
|
+
capturedApiKey = key;
|
|
1019
1189
|
}
|
|
1020
1190
|
}
|
|
1021
1191
|
}
|
|
@@ -1029,6 +1199,7 @@ export async function runSetup(opts) {
|
|
|
1029
1199
|
console.log(` ${CHECK} Key ${existingKey ? "moved" : "saved"} to GNOME Keyring (${maskKey(key)})`);
|
|
1030
1200
|
apiKeyWritten = true;
|
|
1031
1201
|
apiKeySource = "GNOME Keyring";
|
|
1202
|
+
capturedApiKey = key;
|
|
1032
1203
|
}
|
|
1033
1204
|
else {
|
|
1034
1205
|
console.log(` ${CROSS} Failed to write to GNOME Keyring. Falling back to .env file.`);
|
|
@@ -1036,6 +1207,7 @@ export async function runSetup(opts) {
|
|
|
1036
1207
|
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
|
|
1037
1208
|
apiKeyWritten = true;
|
|
1038
1209
|
apiKeySource = "~/.config/gnosys/.env";
|
|
1210
|
+
capturedApiKey = key;
|
|
1039
1211
|
}
|
|
1040
1212
|
}
|
|
1041
1213
|
}
|
|
@@ -1071,6 +1243,7 @@ export async function runSetup(opts) {
|
|
|
1071
1243
|
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
|
|
1072
1244
|
apiKeyWritten = true;
|
|
1073
1245
|
apiKeySource = "~/.config/gnosys/.env";
|
|
1246
|
+
capturedApiKey = key;
|
|
1074
1247
|
}
|
|
1075
1248
|
}
|
|
1076
1249
|
else {
|
|
@@ -1095,6 +1268,42 @@ export async function runSetup(opts) {
|
|
|
1095
1268
|
console.log();
|
|
1096
1269
|
console.log(`${DIM}Step 3/5 \u2014 API key: not needed (local provider)${RESET}`);
|
|
1097
1270
|
}
|
|
1271
|
+
// ─── Validate model with a quick test call ────────────────────────
|
|
1272
|
+
// Only attempt validation when we have what we need: a chosen model,
|
|
1273
|
+
// and either a captured key (for cloud providers) or a local provider
|
|
1274
|
+
// (which doesn't need a key).
|
|
1275
|
+
const isLocalProvider = provider === "ollama" || provider === "lmstudio";
|
|
1276
|
+
const canValidate = !isSkip && model && (capturedApiKey || isLocalProvider);
|
|
1277
|
+
if (canValidate) {
|
|
1278
|
+
console.log();
|
|
1279
|
+
console.log(`${DIM}Testing ${provider}/${model}...${RESET}`);
|
|
1280
|
+
try {
|
|
1281
|
+
const { validateModel } = await import("./modelValidation.js");
|
|
1282
|
+
const customBaseUrl = provider === "custom"
|
|
1283
|
+
? process.env.GNOSYS_LLM_BASE_URL
|
|
1284
|
+
: undefined;
|
|
1285
|
+
const result = await validateModel(provider, model, capturedApiKey, { customBaseUrl });
|
|
1286
|
+
if (result.ok) {
|
|
1287
|
+
console.log(` ${CHECK} Model validated (${result.latencyMs}ms)`);
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
console.log(` ${WARN} Model test failed: ${result.error}`);
|
|
1291
|
+
const proceed = await askYesNo(rl, " Continue anyway?", true);
|
|
1292
|
+
if (!proceed) {
|
|
1293
|
+
console.log(` ${DIM}Setup paused. Re-run when ready: gnosys setup${RESET}`);
|
|
1294
|
+
setupCompleted = true;
|
|
1295
|
+
rl.close();
|
|
1296
|
+
return {
|
|
1297
|
+
provider, model, structuringModel: "",
|
|
1298
|
+
apiKeyWritten, ides: [], mode: "agent", upgraded,
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
catch (err) {
|
|
1304
|
+
console.log(` ${DIM}Validation skipped: ${err instanceof Error ? err.message : err}${RESET}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1098
1307
|
// ─── Step 4/5 — Task Model Configuration ─────────────────────────
|
|
1099
1308
|
const taskOverrides = {};
|
|
1100
1309
|
let dreamEnabled = existingConfig?.dream?.enabled ?? false;
|
|
@@ -1224,11 +1433,17 @@ export async function runSetup(opts) {
|
|
|
1224
1433
|
console.log();
|
|
1225
1434
|
const ideLabels = {
|
|
1226
1435
|
claude: "Claude Code",
|
|
1436
|
+
"claude-desktop": "Claude Desktop",
|
|
1227
1437
|
cursor: "Cursor",
|
|
1228
1438
|
codex: "Codex",
|
|
1439
|
+
"gemini-cli": "Gemini CLI",
|
|
1440
|
+
antigravity: "Antigravity",
|
|
1229
1441
|
};
|
|
1442
|
+
// IDEs whose MCP config lives at the user level (~/...) rather than per-project.
|
|
1443
|
+
// We don't try to create a project-level directory for these.
|
|
1444
|
+
const userLevelIdes = new Set(["claude", "claude-desktop", "gemini-cli", "antigravity"]);
|
|
1230
1445
|
// Build IDE options: show detected ones and offer to create missing ones
|
|
1231
|
-
const allIdeKeys = ["claude", "cursor", "codex"];
|
|
1446
|
+
const allIdeKeys = ["claude", "claude-desktop", "cursor", "codex", "gemini-cli", "antigravity"];
|
|
1232
1447
|
const ideOptions = [];
|
|
1233
1448
|
const ideKeyForOption = []; // parallel array mapping option index to IDE key
|
|
1234
1449
|
for (const ide of allIdeKeys) {
|
|
@@ -1237,12 +1452,13 @@ export async function runSetup(opts) {
|
|
|
1237
1452
|
if (isDetected) {
|
|
1238
1453
|
ideOptions.push(`${label} (detected)`);
|
|
1239
1454
|
}
|
|
1240
|
-
else if (ide
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1455
|
+
else if (userLevelIdes.has(ide)) {
|
|
1456
|
+
// User-level IDEs \u2014 config goes under ~/. We can still write the config
|
|
1457
|
+
// even if the IDE isn't installed yet (it will be picked up later).
|
|
1458
|
+
ideOptions.push(`${label} ${DIM}(not detected \u2014 will configure anyway)${RESET}`);
|
|
1243
1459
|
}
|
|
1244
1460
|
else {
|
|
1245
|
-
//
|
|
1461
|
+
// Project-level IDEs \u2014 offer to create the local directory
|
|
1246
1462
|
ideOptions.push(`${label} ${DIM}(create .${ide}/ \u2014 not detected)${RESET}`);
|
|
1247
1463
|
}
|
|
1248
1464
|
ideKeyForOption.push(ide);
|
|
@@ -1268,8 +1484,10 @@ export async function runSetup(opts) {
|
|
|
1268
1484
|
}
|
|
1269
1485
|
// Last option is "Skip"
|
|
1270
1486
|
for (const ide of idesToSetup) {
|
|
1271
|
-
// For non-detected IDEs
|
|
1272
|
-
|
|
1487
|
+
// For non-detected project-level IDEs, create the directory first.
|
|
1488
|
+
// User-level IDEs (claude, gemini-cli, antigravity) handle their own
|
|
1489
|
+
// ~/-level config dirs inside setupIDE().
|
|
1490
|
+
if (!detectedIdes.includes(ide) && !userLevelIdes.has(ide)) {
|
|
1273
1491
|
const dirPath = path.join(projectDir, `.${ide}`);
|
|
1274
1492
|
try {
|
|
1275
1493
|
await fs.mkdir(dirPath, { recursive: true });
|
|
@@ -1310,7 +1528,7 @@ export async function runSetup(opts) {
|
|
|
1310
1528
|
// Determine which store path to write to — prefer project, fall back to global
|
|
1311
1529
|
let storePath;
|
|
1312
1530
|
const projectStore = path.join(projectDir, ".gnosys");
|
|
1313
|
-
const globalStore =
|
|
1531
|
+
const globalStore = getGnosysHome();
|
|
1314
1532
|
if (fsSync.existsSync(path.join(projectStore, "gnosys.json"))) {
|
|
1315
1533
|
storePath = projectStore;
|
|
1316
1534
|
}
|
|
@@ -1441,13 +1659,9 @@ export async function runSetup(opts) {
|
|
|
1441
1659
|
const { runConfigureWizard } = await import("./remoteWizard.js");
|
|
1442
1660
|
const centralDb = GnosysDB.openCentral();
|
|
1443
1661
|
if (centralDb.isAvailable()) {
|
|
1444
|
-
//
|
|
1445
|
-
rl
|
|
1446
|
-
remoteConfigured = await runConfigureWizard(centralDb);
|
|
1662
|
+
// Pass our readline to the wizard — it will use ours and not close it
|
|
1663
|
+
remoteConfigured = await runConfigureWizard(centralDb, rl);
|
|
1447
1664
|
centralDb.close();
|
|
1448
|
-
// We can't reopen readline after the wizard closed stdin, so
|
|
1449
|
-
// we need to skip any further prompts. Return early after the
|
|
1450
|
-
// summary box.
|
|
1451
1665
|
}
|
|
1452
1666
|
else {
|
|
1453
1667
|
console.log("Central DB not available — skipping remote sync.");
|
|
@@ -1470,11 +1684,7 @@ export async function runSetup(opts) {
|
|
|
1470
1684
|
console.log(`Next: Run ${CYAN}gnosys init${RESET} in any project to start using memory.`);
|
|
1471
1685
|
console.log();
|
|
1472
1686
|
setupCompleted = true;
|
|
1473
|
-
|
|
1474
|
-
try {
|
|
1475
|
-
rl.close();
|
|
1476
|
-
}
|
|
1477
|
-
catch { /* already closed */ }
|
|
1687
|
+
rl.close();
|
|
1478
1688
|
return {
|
|
1479
1689
|
provider: isSkip ? "skip" : provider,
|
|
1480
1690
|
model,
|
|
@@ -1492,4 +1702,228 @@ export async function runSetup(opts) {
|
|
|
1492
1702
|
throw err;
|
|
1493
1703
|
}
|
|
1494
1704
|
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Models-only configuration — prompts for provider, model, and key (or accepts
|
|
1707
|
+
* them via options for non-interactive use). Validates the model against the
|
|
1708
|
+
* provider, then writes the result to gnosys.json. Skips IDE and remote setup.
|
|
1709
|
+
*/
|
|
1710
|
+
export async function runModelsSetup(opts = {}) {
|
|
1711
|
+
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
1712
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1713
|
+
try {
|
|
1714
|
+
console.log();
|
|
1715
|
+
console.log(`${BOLD}${CYAN}Gnosys${RESET} ${DIM}— Model Configuration${RESET}`);
|
|
1716
|
+
console.log();
|
|
1717
|
+
const existingConfig = await loadExistingConfig(projectDir);
|
|
1718
|
+
const currentProvider = existingConfig?.llm.defaultProvider;
|
|
1719
|
+
const currentModel = existingConfig
|
|
1720
|
+
? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
|
|
1721
|
+
: undefined;
|
|
1722
|
+
// Step 1: provider (or use --provider flag)
|
|
1723
|
+
console.log(`${DIM}Fetching latest model pricing...${RESET}`);
|
|
1724
|
+
const dynamicModels = await fetchDynamicModels();
|
|
1725
|
+
if (Object.keys(dynamicModels).length > 0) {
|
|
1726
|
+
console.log(`${DIM}${CHECK} Live pricing loaded from OpenRouter${RESET}`);
|
|
1727
|
+
}
|
|
1728
|
+
console.log();
|
|
1729
|
+
let provider;
|
|
1730
|
+
if (opts.provider) {
|
|
1731
|
+
if (!PROVIDER_ORDER.includes(opts.provider)) {
|
|
1732
|
+
console.log(`${CROSS} Unknown provider: ${opts.provider}`);
|
|
1733
|
+
console.log(` Valid: ${PROVIDER_ORDER.join(", ")}`);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
provider = opts.provider;
|
|
1737
|
+
console.log(`Provider: ${GREEN}${provider}${RESET}`);
|
|
1738
|
+
}
|
|
1739
|
+
else {
|
|
1740
|
+
provider = await pickProvider(rl, dynamicModels, "Choose your LLM provider", currentProvider);
|
|
1741
|
+
}
|
|
1742
|
+
// Step 2: model (or use --model flag)
|
|
1743
|
+
let model;
|
|
1744
|
+
if (opts.model) {
|
|
1745
|
+
model = opts.model;
|
|
1746
|
+
console.log(`Model: ${GREEN}${model}${RESET}`);
|
|
1747
|
+
}
|
|
1748
|
+
else {
|
|
1749
|
+
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
1750
|
+
if (provider === "custom" || !tiers || tiers.length === 0) {
|
|
1751
|
+
model = await askInput(rl, "Model name");
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
const showCurrent = currentProvider === provider ? currentModel : undefined;
|
|
1755
|
+
model = await pickModel(rl, provider, dynamicModels, "Choose model", showCurrent);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (!model) {
|
|
1759
|
+
console.log(`${CROSS} No model selected. Aborting.`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
// Step 3: load API key from existing storage (if available)
|
|
1763
|
+
const envVarName = provider === "custom" ? "GNOSYS_CUSTOM_KEY" :
|
|
1764
|
+
`GNOSYS_${provider.toUpperCase()}_KEY`;
|
|
1765
|
+
const legacyEnvVars = {
|
|
1766
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
1767
|
+
openai: "OPENAI_API_KEY",
|
|
1768
|
+
groq: "GROQ_API_KEY",
|
|
1769
|
+
xai: "XAI_API_KEY",
|
|
1770
|
+
mistral: "MISTRAL_API_KEY",
|
|
1771
|
+
};
|
|
1772
|
+
const legacyEnvVar = legacyEnvVars[provider] ?? "";
|
|
1773
|
+
let apiKey = process.env[envVarName] || (legacyEnvVar ? process.env[legacyEnvVar] : "") || "";
|
|
1774
|
+
if (!apiKey && process.platform === "darwin") {
|
|
1775
|
+
try {
|
|
1776
|
+
apiKey = execSync(`security find-generic-password -a "$USER" -s "${envVarName}" -w`, { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
1777
|
+
}
|
|
1778
|
+
catch {
|
|
1779
|
+
// No key in keychain
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (!apiKey && provider !== "ollama" && provider !== "lmstudio") {
|
|
1783
|
+
console.log(`${WARN} No API key found for ${provider}. Run 'gnosys setup' to configure one.`);
|
|
1784
|
+
// Continue anyway — user might just want to update the model in config
|
|
1785
|
+
}
|
|
1786
|
+
// Step 4: validate (default: true)
|
|
1787
|
+
const shouldValidate = opts.validate !== false;
|
|
1788
|
+
const isLocalProvider = provider === "ollama" || provider === "lmstudio";
|
|
1789
|
+
if (shouldValidate && (apiKey || isLocalProvider)) {
|
|
1790
|
+
console.log();
|
|
1791
|
+
console.log(`${DIM}Testing ${provider}/${model}...${RESET}`);
|
|
1792
|
+
const customBaseUrl = provider === "custom"
|
|
1793
|
+
? process.env.GNOSYS_LLM_BASE_URL
|
|
1794
|
+
: undefined;
|
|
1795
|
+
const result = await validateModel(provider, model, apiKey, { customBaseUrl });
|
|
1796
|
+
if (result.ok) {
|
|
1797
|
+
console.log(` ${CHECK} Model validated (${result.latencyMs}ms)`);
|
|
1798
|
+
}
|
|
1799
|
+
else {
|
|
1800
|
+
console.log(` ${WARN} Model test failed: ${result.error}`);
|
|
1801
|
+
const proceed = await askYesNo(rl, " Save config anyway?", false);
|
|
1802
|
+
if (!proceed) {
|
|
1803
|
+
console.log(` ${DIM}Cancelled.${RESET}`);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
// Step 5: write config
|
|
1809
|
+
const projectStore = path.join(projectDir, ".gnosys");
|
|
1810
|
+
const globalStore = getGnosysHome();
|
|
1811
|
+
let storePath;
|
|
1812
|
+
if (fsSync.existsSync(path.join(projectStore, "gnosys.json"))) {
|
|
1813
|
+
storePath = projectStore;
|
|
1814
|
+
}
|
|
1815
|
+
else if (fsSync.existsSync(path.join(globalStore, "gnosys.json"))) {
|
|
1816
|
+
storePath = globalStore;
|
|
1817
|
+
}
|
|
1818
|
+
else {
|
|
1819
|
+
await fs.mkdir(globalStore, { recursive: true });
|
|
1820
|
+
storePath = globalStore;
|
|
1821
|
+
}
|
|
1822
|
+
const existingLlm = existingConfig?.llm;
|
|
1823
|
+
const existingProviderConfig = existingLlm
|
|
1824
|
+
? existingLlm[provider]
|
|
1825
|
+
: undefined;
|
|
1826
|
+
const providerConfigBase = (typeof existingProviderConfig === "object" && existingProviderConfig !== null)
|
|
1827
|
+
? existingProviderConfig
|
|
1828
|
+
: {};
|
|
1829
|
+
await updateConfig(storePath, {
|
|
1830
|
+
llm: {
|
|
1831
|
+
...(existingLlm ?? {}),
|
|
1832
|
+
defaultProvider: provider,
|
|
1833
|
+
[provider]: {
|
|
1834
|
+
...providerConfigBase,
|
|
1835
|
+
model,
|
|
1836
|
+
},
|
|
1837
|
+
},
|
|
1838
|
+
});
|
|
1839
|
+
console.log();
|
|
1840
|
+
console.log(` ${CHECK} Config saved: ${storePath}/gnosys.json`);
|
|
1841
|
+
console.log(` ${DIM}Provider: ${provider}${RESET}`);
|
|
1842
|
+
console.log(` ${DIM}Model: ${model}${RESET}`);
|
|
1843
|
+
}
|
|
1844
|
+
finally {
|
|
1845
|
+
rl.close();
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Lightweight model-management command. Supports three operations:
|
|
1850
|
+
* --list: print available models for the current provider
|
|
1851
|
+
* --refresh: clear the OpenRouter cache and re-fetch
|
|
1852
|
+
* --set X: update the default model in gnosys.json (no prompts)
|
|
1853
|
+
*/
|
|
1854
|
+
export async function runModelsCommand(opts = {}) {
|
|
1855
|
+
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
1856
|
+
const existingConfig = await loadExistingConfig(projectDir);
|
|
1857
|
+
const currentProvider = existingConfig?.llm.defaultProvider;
|
|
1858
|
+
if (opts.refresh) {
|
|
1859
|
+
const cacheFile = path.join(os.homedir(), ".config", "gnosys", "models-cache.json");
|
|
1860
|
+
try {
|
|
1861
|
+
await fs.unlink(cacheFile);
|
|
1862
|
+
console.log(`${CHECK} Cache cleared.`);
|
|
1863
|
+
}
|
|
1864
|
+
catch {
|
|
1865
|
+
console.log(`${DIM}No cache to clear.${RESET}`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if (opts.list) {
|
|
1869
|
+
if (!currentProvider) {
|
|
1870
|
+
console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
console.log();
|
|
1874
|
+
console.log(`${BOLD}Available models for ${currentProvider}:${RESET}`);
|
|
1875
|
+
console.log();
|
|
1876
|
+
const dynamicModels = await fetchDynamicModels();
|
|
1877
|
+
const tiers = dynamicModels[currentProvider] ?? PROVIDER_TIERS[currentProvider] ?? [];
|
|
1878
|
+
if (tiers.length === 0) {
|
|
1879
|
+
console.log(` ${DIM}No models in catalog. Try '--refresh' or use a custom model name.${RESET}`);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
for (const t of tiers) {
|
|
1883
|
+
const rec = t.recommended ? ` ${CYAN}<- recommended${RESET}` : "";
|
|
1884
|
+
const price = t.input === 0 && t.output === 0
|
|
1885
|
+
? "free"
|
|
1886
|
+
: `$${t.input.toFixed(2)}–$${t.output.toFixed(2)}/M`;
|
|
1887
|
+
console.log(` ${t.name.padEnd(24)} ${t.model.padEnd(40)} ${DIM}${price}${RESET}${rec}`);
|
|
1888
|
+
}
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (opts.set) {
|
|
1892
|
+
if (!currentProvider) {
|
|
1893
|
+
console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const projectStore = path.join(projectDir, ".gnosys");
|
|
1897
|
+
const globalStore = getGnosysHome();
|
|
1898
|
+
const storePath = fsSync.existsSync(path.join(projectStore, "gnosys.json"))
|
|
1899
|
+
? projectStore
|
|
1900
|
+
: globalStore;
|
|
1901
|
+
const existingProviderConfig = existingConfig?.llm?.[currentProvider];
|
|
1902
|
+
const providerConfigBase = (typeof existingProviderConfig === "object" && existingProviderConfig !== null)
|
|
1903
|
+
? existingProviderConfig
|
|
1904
|
+
: {};
|
|
1905
|
+
await updateConfig(storePath, {
|
|
1906
|
+
llm: {
|
|
1907
|
+
...(existingConfig?.llm ?? {}),
|
|
1908
|
+
defaultProvider: currentProvider,
|
|
1909
|
+
[currentProvider]: { ...providerConfigBase, model: opts.set },
|
|
1910
|
+
},
|
|
1911
|
+
});
|
|
1912
|
+
console.log(`${CHECK} Default model set to ${GREEN}${opts.set}${RESET} for ${currentProvider}.`);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
// No flags: show current config
|
|
1916
|
+
if (!currentProvider) {
|
|
1917
|
+
console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
const currentModel = existingConfig
|
|
1921
|
+
? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
|
|
1922
|
+
: "";
|
|
1923
|
+
console.log();
|
|
1924
|
+
console.log(`Provider: ${GREEN}${currentProvider}${RESET}`);
|
|
1925
|
+
console.log(`Model: ${GREEN}${currentModel}${RESET}`);
|
|
1926
|
+
console.log();
|
|
1927
|
+
console.log(`${DIM}Use '--list' to see options, '--set <model>' to change, '--refresh' to update catalog.${RESET}`);
|
|
1928
|
+
}
|
|
1495
1929
|
//# sourceMappingURL=setup.js.map
|