gnosys 5.11.4 → 5.12.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 +324 -5150
- package/dist/index.js +364 -235
- package/dist/lib/addCommand.d.ts +9 -0
- package/dist/lib/addCommand.js +103 -0
- package/dist/lib/addStructuredCommand.d.ts +16 -0
- package/dist/lib/addStructuredCommand.js +103 -0
- package/dist/lib/ambiguityCommand.d.ts +4 -0
- package/dist/lib/ambiguityCommand.js +36 -0
- package/dist/lib/apiKeyVault.d.ts +78 -0
- package/dist/lib/apiKeyVault.js +447 -0
- package/dist/lib/askCommand.d.ts +13 -0
- package/dist/lib/askCommand.js +145 -0
- package/dist/lib/audioExtract.js +4 -1
- package/dist/lib/auditCommand.d.ts +7 -0
- package/dist/lib/auditCommand.js +27 -0
- package/dist/lib/backupCommand.d.ts +6 -0
- package/dist/lib/backupCommand.js +54 -0
- package/dist/lib/bootstrapCommand.d.ts +15 -0
- package/dist/lib/bootstrapCommand.js +51 -0
- package/dist/lib/briefingCommand.d.ts +7 -0
- package/dist/lib/briefingCommand.js +92 -0
- package/dist/lib/centralizeCommand.d.ts +5 -0
- package/dist/lib/centralizeCommand.js +16 -0
- package/dist/lib/chatCommand.d.ts +12 -0
- package/dist/lib/chatCommand.js +46 -0
- package/dist/lib/checkCommand.d.ts +4 -0
- package/dist/lib/checkCommand.js +133 -0
- package/dist/lib/clientReadOverlay.d.ts +27 -0
- package/dist/lib/clientReadOverlay.js +73 -0
- package/dist/lib/clientReadResolve.d.ts +32 -0
- package/dist/lib/clientReadResolve.js +84 -0
- package/dist/lib/commitContextCommand.d.ts +9 -0
- package/dist/lib/commitContextCommand.js +142 -0
- package/dist/lib/config.d.ts +43 -3
- package/dist/lib/config.js +58 -57
- package/dist/lib/configCommand.d.ts +10 -0
- package/dist/lib/configCommand.js +321 -0
- package/dist/lib/connectCommand.d.ts +8 -0
- package/dist/lib/connectCommand.js +19 -0
- package/dist/lib/db.d.ts +52 -0
- package/dist/lib/db.js +169 -1
- package/dist/lib/dearchiveCommand.d.ts +7 -0
- package/dist/lib/dearchiveCommand.js +41 -0
- package/dist/lib/discoverCommand.d.ts +9 -0
- package/dist/lib/discoverCommand.js +87 -0
- package/dist/lib/doctorCommand.d.ts +6 -0
- package/dist/lib/doctorCommand.js +256 -0
- package/dist/lib/dream.d.ts +42 -2
- package/dist/lib/dream.js +290 -30
- package/dist/lib/dreamCommand.d.ts +10 -0
- package/dist/lib/dreamCommand.js +195 -0
- package/dist/lib/dreamLaunchd.d.ts +2 -0
- package/dist/lib/dreamLaunchd.js +72 -0
- package/dist/lib/dreamLogCommand.d.ts +10 -0
- package/dist/lib/dreamLogCommand.js +58 -0
- package/dist/lib/dreamReport.d.ts +7 -0
- package/dist/lib/dreamReport.js +114 -0
- package/dist/lib/dreamRunLog.d.ts +121 -0
- package/dist/lib/dreamRunLog.js +212 -0
- package/dist/lib/embeddings.js +3 -0
- package/dist/lib/exportCommand.d.ts +18 -0
- package/dist/lib/exportCommand.js +101 -0
- package/dist/lib/fsearchCommand.d.ts +8 -0
- package/dist/lib/fsearchCommand.js +44 -0
- package/dist/lib/graphCommand.d.ts +4 -0
- package/dist/lib/graphCommand.js +68 -0
- package/dist/lib/helperGenerateCommand.d.ts +5 -0
- package/dist/lib/helperGenerateCommand.js +27 -0
- package/dist/lib/historyCommand.d.ts +5 -0
- package/dist/lib/historyCommand.js +51 -0
- package/dist/lib/hybridSearchCommand.d.ts +12 -0
- package/dist/lib/hybridSearchCommand.js +95 -0
- package/dist/lib/importCommand.d.ts +16 -0
- package/dist/lib/importCommand.js +89 -0
- package/dist/lib/importProjectCommand.d.ts +6 -0
- package/dist/lib/importProjectCommand.js +43 -0
- package/dist/lib/ingestCommand.d.ts +13 -0
- package/dist/lib/ingestCommand.js +95 -0
- package/dist/lib/installOutput.d.ts +36 -0
- package/dist/lib/installOutput.js +55 -0
- package/dist/lib/lensCommand.d.ts +20 -0
- package/dist/lib/lensCommand.js +61 -0
- package/dist/lib/lensing.d.ts +1 -0
- package/dist/lib/lensing.js +50 -9
- package/dist/lib/linksCommand.d.ts +7 -0
- package/dist/lib/linksCommand.js +48 -0
- package/dist/lib/listCommand.d.ts +8 -0
- package/dist/lib/listCommand.js +74 -0
- package/dist/lib/llm.d.ts +1 -1
- package/dist/lib/llm.js +26 -8
- package/dist/lib/localDiskCheck.d.ts +17 -0
- package/dist/lib/localDiskCheck.js +54 -0
- package/dist/lib/machineConfig.d.ts +11 -1
- package/dist/lib/machineConfig.js +16 -0
- package/dist/lib/machineRegistry.d.ts +61 -0
- package/dist/lib/machineRegistry.js +80 -0
- package/dist/lib/maintainCommand.d.ts +8 -0
- package/dist/lib/maintainCommand.js +34 -0
- package/dist/lib/masterLease.d.ts +20 -0
- package/dist/lib/masterLease.js +68 -0
- package/dist/lib/migrateCommand.d.ts +7 -0
- package/dist/lib/migrateCommand.js +158 -0
- package/dist/lib/migrateDbCommand.d.ts +9 -0
- package/dist/lib/migrateDbCommand.js +94 -0
- package/dist/lib/modelValidation.d.ts +5 -0
- package/dist/lib/modelValidation.js +27 -0
- package/dist/lib/openrouterTiers.d.ts +29 -0
- package/dist/lib/openrouterTiers.js +113 -0
- package/dist/lib/prefCommand.d.ts +10 -0
- package/dist/lib/prefCommand.js +118 -0
- package/dist/lib/projectsCommand.d.ts +8 -0
- package/dist/lib/projectsCommand.js +131 -0
- package/dist/lib/readCommand.d.ts +7 -0
- package/dist/lib/readCommand.js +62 -0
- package/dist/lib/recall.d.ts +3 -0
- package/dist/lib/recall.js +19 -4
- package/dist/lib/recallCommand.d.ts +11 -0
- package/dist/lib/recallCommand.js +112 -0
- package/dist/lib/reflectCommand.d.ts +8 -0
- package/dist/lib/reflectCommand.js +61 -0
- package/dist/lib/reindexCommand.d.ts +4 -0
- package/dist/lib/reindexCommand.js +34 -0
- package/dist/lib/reindexGraphCommand.d.ts +4 -0
- package/dist/lib/reindexGraphCommand.js +12 -0
- package/dist/lib/reinforceCommand.d.ts +8 -0
- package/dist/lib/reinforceCommand.js +40 -0
- package/dist/lib/remote.d.ts +5 -1
- package/dist/lib/remote.js +5 -1
- package/dist/lib/remoteWizard.d.ts +24 -5
- package/dist/lib/remoteWizard.js +308 -319
- package/dist/lib/restoreCommand.d.ts +5 -0
- package/dist/lib/restoreCommand.js +35 -0
- package/dist/lib/sandboxStartCommand.d.ts +6 -0
- package/dist/lib/sandboxStartCommand.js +25 -0
- package/dist/lib/sandboxStatusCommand.d.ts +4 -0
- package/dist/lib/sandboxStatusCommand.js +24 -0
- package/dist/lib/sandboxStopCommand.d.ts +4 -0
- package/dist/lib/sandboxStopCommand.js +21 -0
- package/dist/lib/searchCommand.d.ts +9 -0
- package/dist/lib/searchCommand.js +90 -0
- package/dist/lib/semanticSearchCommand.d.ts +8 -0
- package/dist/lib/semanticSearchCommand.js +52 -0
- package/dist/lib/setup/configSetRender.js +2 -0
- package/dist/lib/setup/providerGlyphs.d.ts +19 -0
- package/dist/lib/setup/providerGlyphs.js +42 -0
- package/dist/lib/setup/remoteRender.d.ts +31 -1
- package/dist/lib/setup/remoteRender.js +95 -4
- package/dist/lib/setup/sections/providers.d.ts +17 -0
- package/dist/lib/setup/sections/providers.js +255 -0
- package/dist/lib/setup/sections/routing.d.ts +2 -6
- package/dist/lib/setup/sections/routing.js +33 -85
- package/dist/lib/setup/sections/taskRoutingEditor.d.ts +17 -0
- package/dist/lib/setup/sections/taskRoutingEditor.js +149 -0
- package/dist/lib/setup/summary.d.ts +9 -0
- package/dist/lib/setup/summary.js +51 -37
- package/dist/lib/setup/ui/status.d.ts +1 -0
- package/dist/lib/setup/ui/status.js +2 -0
- package/dist/lib/setup.d.ts +108 -3
- package/dist/lib/setup.js +762 -157
- package/dist/lib/setupKeys.d.ts +42 -0
- package/dist/lib/setupKeys.js +564 -0
- package/dist/lib/setupRemoteCommand.d.ts +4 -0
- package/dist/lib/setupRemoteCommand.js +28 -0
- package/dist/lib/setupRemotePullCommand.d.ts +5 -0
- package/dist/lib/setupRemotePullCommand.js +52 -0
- package/dist/lib/setupRemotePushCommand.d.ts +5 -0
- package/dist/lib/setupRemotePushCommand.js +57 -0
- package/dist/lib/setupRemoteResolveCommand.d.ts +4 -0
- package/dist/lib/setupRemoteResolveCommand.js +48 -0
- package/dist/lib/setupRemoteStatusCommand.d.ts +4 -0
- package/dist/lib/setupRemoteStatusCommand.js +73 -0
- package/dist/lib/setupRemoteSyncCommand.d.ts +6 -0
- package/dist/lib/setupRemoteSyncCommand.js +65 -0
- package/dist/lib/setupSyncProjectsCommand.d.ts +4 -0
- package/dist/lib/setupSyncProjectsCommand.js +292 -0
- package/dist/lib/staleCommand.d.ts +8 -0
- package/dist/lib/staleCommand.js +34 -0
- package/dist/lib/statsCommand.d.ts +6 -0
- package/dist/lib/statsCommand.js +142 -0
- package/dist/lib/statusCommand.d.ts +18 -0
- package/dist/lib/statusCommand.js +250 -0
- package/dist/lib/storesCommand.d.ts +2 -0
- package/dist/lib/storesCommand.js +4 -0
- package/dist/lib/syncClient.d.ts +47 -0
- package/dist/lib/syncClient.js +212 -0
- package/dist/lib/syncCommand.d.ts +6 -0
- package/dist/lib/syncCommand.js +57 -0
- package/dist/lib/syncDoctorCommand.d.ts +5 -0
- package/dist/lib/syncDoctorCommand.js +100 -0
- package/dist/lib/syncIngest.d.ts +19 -0
- package/dist/lib/syncIngest.js +152 -0
- package/dist/lib/syncIngestLaunchd.d.ts +8 -0
- package/dist/lib/syncIngestLaunchd.js +93 -0
- package/dist/lib/syncIngestStartup.d.ts +5 -0
- package/dist/lib/syncIngestStartup.js +29 -0
- package/dist/lib/syncIngestSystemd.d.ts +10 -0
- package/dist/lib/syncIngestSystemd.js +97 -0
- package/dist/lib/syncIngestTimer.d.ts +8 -0
- package/dist/lib/syncIngestTimer.js +27 -0
- package/dist/lib/syncIngestTimerCommand.d.ts +7 -0
- package/dist/lib/syncIngestTimerCommand.js +83 -0
- package/dist/lib/syncLock.d.ts +6 -0
- package/dist/lib/syncLock.js +74 -0
- package/dist/lib/syncSnapshot.d.ts +30 -0
- package/dist/lib/syncSnapshot.js +184 -0
- package/dist/lib/syncStaging.d.ts +81 -0
- package/dist/lib/syncStaging.js +239 -0
- package/dist/lib/tagsAddCommand.d.ts +8 -0
- package/dist/lib/tagsAddCommand.js +18 -0
- package/dist/lib/tagsCommand.d.ts +4 -0
- package/dist/lib/tagsCommand.js +16 -0
- package/dist/lib/timelineCommand.d.ts +7 -0
- package/dist/lib/timelineCommand.js +49 -0
- package/dist/lib/traceCommand.d.ts +6 -0
- package/dist/lib/traceCommand.js +39 -0
- package/dist/lib/traverseCommand.d.ts +6 -0
- package/dist/lib/traverseCommand.js +58 -0
- package/dist/lib/updateCommand.d.ts +13 -0
- package/dist/lib/updateCommand.js +67 -0
- package/dist/lib/updateStatusCommand.d.ts +5 -0
- package/dist/lib/updateStatusCommand.js +38 -0
- package/dist/lib/webAddCommand.d.ts +8 -0
- package/dist/lib/webAddCommand.js +55 -0
- package/dist/lib/webBuildCommand.d.ts +10 -0
- package/dist/lib/webBuildCommand.js +65 -0
- package/dist/lib/webBuildIndexCommand.d.ts +8 -0
- package/dist/lib/webBuildIndexCommand.js +37 -0
- package/dist/lib/webIngestCommand.d.ts +11 -0
- package/dist/lib/webIngestCommand.js +51 -0
- package/dist/lib/webInitCommand.d.ts +9 -0
- package/dist/lib/webInitCommand.js +167 -0
- package/dist/lib/webRemoveCommand.d.ts +5 -0
- package/dist/lib/webRemoveCommand.js +41 -0
- package/dist/lib/webStatusCommand.d.ts +5 -0
- package/dist/lib/webStatusCommand.js +94 -0
- package/dist/lib/webUpdateCommand.d.ts +7 -0
- package/dist/lib/webUpdateCommand.js +72 -0
- package/dist/lib/workingSetCommand.d.ts +6 -0
- package/dist/lib/workingSetCommand.js +37 -0
- package/package.json +2 -1
package/dist/lib/setup.js
CHANGED
|
@@ -14,12 +14,16 @@ import fsSync from "fs";
|
|
|
14
14
|
import path from "path";
|
|
15
15
|
import os from "os";
|
|
16
16
|
import { execSync } from "child_process";
|
|
17
|
-
import { loadConfig, updateConfig, getProviderModel, } from "./config.js";
|
|
18
|
-
import { validateModel } from "./modelValidation.js";
|
|
17
|
+
import { loadConfig, updateConfig, resolveTaskModel, getProviderModel, } from "./config.js";
|
|
18
|
+
import { isApiKeyValidationError, validateModel } from "./modelValidation.js";
|
|
19
|
+
import { buildOpenRouterTiers, OPENROUTER_STATIC_TIERS, } from "./openrouterTiers.js";
|
|
20
|
+
import { apiKeyServiceName, buildApiKeyRequirementsFromConfig, ensureApiKeys, getApiKeyForProviderFromConfig, providerNeedsApiKey, readFirstInChain, readStoredSecret, storeApiKeySecret, } from "./apiKeyVault.js";
|
|
19
21
|
import { resolveActiveStorePath, ensureActiveStorePath } from "./setup/storePath.js";
|
|
20
22
|
import { safeQuestion } from "./setup/ui/safePrompt.js";
|
|
23
|
+
export { printStatus } from "./setup/ui/status.js";
|
|
21
24
|
import { getClaudeDesktopConfigPath, getApiKeySkipHints } from "./platform.js";
|
|
22
25
|
import { gnosysStdioMcpEntry, installStdioMcpJson, normalizeIdeKey, resolveGnosysMcpCommand, cursorMcpPaths, runCli, removeTomlSection, } from "./ideMcpInstall.js";
|
|
26
|
+
import { runKeysSetup } from "./setupKeys.js";
|
|
23
27
|
// ─── ANSI Colors ────────────────────────────────────────────────────────────
|
|
24
28
|
const BOLD = "\x1b[1m";
|
|
25
29
|
const DIM = "\x1b[2m";
|
|
@@ -76,6 +80,7 @@ export const PROVIDER_TIERS = {
|
|
|
76
80
|
lmstudio: [
|
|
77
81
|
{ name: "Default", model: "default", input: 0, output: 0, recommended: true },
|
|
78
82
|
],
|
|
83
|
+
openrouter: OPENROUTER_STATIC_TIERS,
|
|
79
84
|
custom: [],
|
|
80
85
|
};
|
|
81
86
|
// ─── Dynamic Model Fetching (OpenRouter) ─────────────────────────────────────
|
|
@@ -95,7 +100,7 @@ const OPENROUTER_PREFIX_MAP = {
|
|
|
95
100
|
* Fetch models from OpenRouter, cache for 24 hours, fall back to hardcoded.
|
|
96
101
|
* Returns updated PROVIDER_TIERS for cloud providers only.
|
|
97
102
|
*/
|
|
98
|
-
async function fetchDynamicModels() {
|
|
103
|
+
export async function fetchDynamicModels() {
|
|
99
104
|
// Check cache first
|
|
100
105
|
try {
|
|
101
106
|
const stat = await fs.stat(CACHE_FILE);
|
|
@@ -250,6 +255,7 @@ async function fetchDynamicModels() {
|
|
|
250
255
|
}
|
|
251
256
|
result[ourProvider] = tiers;
|
|
252
257
|
}
|
|
258
|
+
result.openrouter = buildOpenRouterTiers(data.data);
|
|
253
259
|
// Cache the result
|
|
254
260
|
try {
|
|
255
261
|
await fs.mkdir(path.dirname(CACHE_FILE), { recursive: true });
|
|
@@ -284,6 +290,7 @@ const PROVIDER_DISPLAY = {
|
|
|
284
290
|
xai: "xAI (Grok)",
|
|
285
291
|
mistral: "Mistral",
|
|
286
292
|
lmstudio: "LM Studio (local, free)",
|
|
293
|
+
openrouter: "OpenRouter (free + paid models)",
|
|
287
294
|
custom: "Custom (any OpenAI-compatible API)",
|
|
288
295
|
};
|
|
289
296
|
const PROVIDER_ENV_VAR = {
|
|
@@ -292,6 +299,7 @@ const PROVIDER_ENV_VAR = {
|
|
|
292
299
|
groq: "GNOSYS_GROQ_KEY",
|
|
293
300
|
xai: "GNOSYS_XAI_KEY",
|
|
294
301
|
mistral: "GNOSYS_MISTRAL_KEY",
|
|
302
|
+
openrouter: "GNOSYS_OPENROUTER_KEY",
|
|
295
303
|
custom: "GNOSYS_CUSTOM_KEY",
|
|
296
304
|
};
|
|
297
305
|
// Ordered list for the menu
|
|
@@ -303,16 +311,40 @@ const PROVIDER_ORDER = [
|
|
|
303
311
|
"xai",
|
|
304
312
|
"mistral",
|
|
305
313
|
"lmstudio",
|
|
314
|
+
"openrouter",
|
|
306
315
|
"custom",
|
|
307
316
|
];
|
|
308
317
|
// Task descriptions for display
|
|
309
|
-
const TASK_DESCRIPTIONS = {
|
|
318
|
+
export const TASK_DESCRIPTIONS = {
|
|
310
319
|
structuring: "adding memories, tagging",
|
|
311
320
|
synthesis: "Q&A answers",
|
|
321
|
+
chat: "interactive chat TUI",
|
|
312
322
|
vision: "images, PDFs",
|
|
313
323
|
transcription: "audio files",
|
|
314
324
|
dream: "overnight consolidation",
|
|
315
325
|
};
|
|
326
|
+
/** Tasks routed via taskModels in gnosys.json */
|
|
327
|
+
const ROUTABLE_TASK_LIST = [
|
|
328
|
+
"structuring",
|
|
329
|
+
"synthesis",
|
|
330
|
+
"vision",
|
|
331
|
+
"transcription",
|
|
332
|
+
"chat",
|
|
333
|
+
];
|
|
334
|
+
export const ASSIGNABLE_TASK_LIST = [
|
|
335
|
+
...ROUTABLE_TASK_LIST,
|
|
336
|
+
"dream",
|
|
337
|
+
];
|
|
338
|
+
export function getAssignableRouting(cfg, task) {
|
|
339
|
+
if (task === "dream") {
|
|
340
|
+
const dreamProvider = (cfg.dream?.provider ?? "ollama");
|
|
341
|
+
return {
|
|
342
|
+
provider: dreamProvider,
|
|
343
|
+
model: cfg.dream?.model ?? getProviderModel(cfg, dreamProvider),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return resolveTaskModel(cfg, task);
|
|
347
|
+
}
|
|
316
348
|
// ─── Exported Helpers ───────────────────────────────────────────────────────
|
|
317
349
|
/**
|
|
318
350
|
* Returns the cheapest capable model for structuring tasks.
|
|
@@ -334,8 +366,10 @@ export function getStructuringModel(provider, chosenModel) {
|
|
|
334
366
|
* Creates the directory and file if they don't exist.
|
|
335
367
|
* Replaces an existing key line if found, otherwise appends.
|
|
336
368
|
*/
|
|
337
|
-
export async function writeApiKey(provider, key) {
|
|
338
|
-
const envVar =
|
|
369
|
+
export async function writeApiKey(provider, key, opts) {
|
|
370
|
+
const envVar = opts?.scope === "global"
|
|
371
|
+
? apiKeyServiceName(provider, "global")
|
|
372
|
+
: PROVIDER_ENV_VAR[provider];
|
|
339
373
|
if (!envVar)
|
|
340
374
|
return;
|
|
341
375
|
const configDir = path.join(os.homedir(), ".config", "gnosys");
|
|
@@ -418,6 +452,68 @@ function hasSecretTool() {
|
|
|
418
452
|
return false;
|
|
419
453
|
}
|
|
420
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Prompt for a replacement API key after validation fails, storing it in the
|
|
457
|
+
* platform secure store when available (Keychain / GNOME Keyring), else .env.
|
|
458
|
+
*/
|
|
459
|
+
async function promptAndStoreProviderApiKey(rl, provider, scope = "provider") {
|
|
460
|
+
if (provider === "ollama" || provider === "lmstudio" || provider === "skip") {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
const service = apiKeyServiceName(provider, scope);
|
|
464
|
+
console.log();
|
|
465
|
+
console.log(` ${WARN} Your API key may be expired or invalid.`);
|
|
466
|
+
console.log(` ${DIM}Enter a new key — saved as ${service}${RESET}`);
|
|
467
|
+
console.log();
|
|
468
|
+
const key = await askInput(rl, `Enter your ${provider} API key`);
|
|
469
|
+
if (!key) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
if (storeApiKeySecret(service, key, provider)) {
|
|
473
|
+
const store = process.platform === "darwin" ? "macOS Keychain" : "GNOME Keyring";
|
|
474
|
+
console.log(` ${CHECK} Key saved to ${store} (${maskKey(key)})`);
|
|
475
|
+
return key;
|
|
476
|
+
}
|
|
477
|
+
console.log(` ${CROSS} Failed to write to secure store. Saving to ~/.config/gnosys/.env instead.`);
|
|
478
|
+
await writeApiKey(provider, key);
|
|
479
|
+
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
|
|
480
|
+
return key;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Run model validation; on API-key auth failures, offer to rotate the key and
|
|
484
|
+
* retry once before asking whether to save config anyway.
|
|
485
|
+
*/
|
|
486
|
+
async function validateModelWithKeyRotation(opts) {
|
|
487
|
+
const { Spinner } = await import("./setup/ui/spinner.js");
|
|
488
|
+
let apiKey = opts.apiKey;
|
|
489
|
+
const validateSpin = Spinner(`validating ${opts.provider} / ${opts.model}…`);
|
|
490
|
+
let result = await validateModel(opts.provider, opts.model, apiKey, {
|
|
491
|
+
customBaseUrl: opts.customBaseUrl,
|
|
492
|
+
});
|
|
493
|
+
if (result.ok) {
|
|
494
|
+
validateSpin.ok("model validated", `${result.latencyMs} ms · ${opts.provider} / ${opts.model}`);
|
|
495
|
+
return { proceed: true, apiKey };
|
|
496
|
+
}
|
|
497
|
+
validateSpin.fail("model test failed", result.error);
|
|
498
|
+
if (!opts.isLocalProvider &&
|
|
499
|
+
isApiKeyValidationError(result.error)) {
|
|
500
|
+
const newKey = await promptAndStoreProviderApiKey(opts.rl, opts.provider, opts.keyScope ?? "global");
|
|
501
|
+
if (newKey) {
|
|
502
|
+
apiKey = newKey;
|
|
503
|
+
const retrySpin = Spinner(`re-validating ${opts.provider} / ${opts.model}…`);
|
|
504
|
+
result = await validateModel(opts.provider, opts.model, apiKey, {
|
|
505
|
+
customBaseUrl: opts.customBaseUrl,
|
|
506
|
+
});
|
|
507
|
+
if (result.ok) {
|
|
508
|
+
retrySpin.ok("model validated", `${result.latencyMs} ms · ${opts.provider} / ${opts.model}`);
|
|
509
|
+
return { proceed: true, apiKey };
|
|
510
|
+
}
|
|
511
|
+
retrySpin.fail("model test failed", result.error);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const proceed = await askYesNo(opts.rl, opts.saveAnywayPrompt, opts.saveAnywayDefault);
|
|
515
|
+
return { proceed, apiKey };
|
|
516
|
+
}
|
|
421
517
|
/**
|
|
422
518
|
* Detect where an existing API key is stored.
|
|
423
519
|
* Returns description like "macOS Keychain", "env var", ".env file", or empty string.
|
|
@@ -817,16 +913,20 @@ async function askChoice(rl, question, options) {
|
|
|
817
913
|
/**
|
|
818
914
|
* Read a single line of input with an optional default value.
|
|
819
915
|
*/
|
|
820
|
-
async function askInput(rl, prompt, opts) {
|
|
916
|
+
export async function askInput(rl, prompt, opts) {
|
|
821
917
|
const suffix = opts?.default ? ` ${DIM}(${opts.default})${RESET}` : "";
|
|
822
918
|
const answer = await safeQuestion(rl, `${prompt}${suffix}: `);
|
|
823
919
|
const trimmed = answer.trim();
|
|
824
920
|
return trimmed || opts?.default || "";
|
|
825
921
|
}
|
|
922
|
+
export function printInfo(text, meta) {
|
|
923
|
+
const suffix = meta ? ` ${DIM}${meta}${RESET}` : "";
|
|
924
|
+
console.log(` ${text}${suffix}`);
|
|
925
|
+
}
|
|
826
926
|
/**
|
|
827
927
|
* Y/n prompt. Returns true for yes.
|
|
828
928
|
*/
|
|
829
|
-
async function askYesNo(rl, question, defaultYes = true) {
|
|
929
|
+
export async function askYesNo(rl, question, defaultYes = true) {
|
|
830
930
|
const hint = defaultYes ? "Y/n" : "y/N";
|
|
831
931
|
const answer = await safeQuestion(rl, `${question} [${hint}] `);
|
|
832
932
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -834,6 +934,78 @@ async function askYesNo(rl, question, defaultYes = true) {
|
|
|
834
934
|
return defaultYes;
|
|
835
935
|
return trimmed === "y" || trimmed === "yes";
|
|
836
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Read a password/API key without echoing it back to the terminal.
|
|
939
|
+
*/
|
|
940
|
+
export async function askPassword(rl, prompt) {
|
|
941
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
942
|
+
return askInput(rl, prompt);
|
|
943
|
+
}
|
|
944
|
+
return new Promise((resolve) => {
|
|
945
|
+
let value = "";
|
|
946
|
+
stdout.write(`${prompt}: `);
|
|
947
|
+
stdin.setRawMode(true);
|
|
948
|
+
stdin.resume();
|
|
949
|
+
const cleanup = () => {
|
|
950
|
+
stdin.setRawMode(false);
|
|
951
|
+
stdin.off("data", onData);
|
|
952
|
+
stdout.write("\n");
|
|
953
|
+
};
|
|
954
|
+
const onData = (chunk) => {
|
|
955
|
+
const input = chunk.toString("utf-8");
|
|
956
|
+
for (const char of input) {
|
|
957
|
+
const code = char.charCodeAt(0);
|
|
958
|
+
if (code === 3) {
|
|
959
|
+
cleanup();
|
|
960
|
+
try {
|
|
961
|
+
rl.close();
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
// already closed
|
|
965
|
+
}
|
|
966
|
+
process.exit(130);
|
|
967
|
+
}
|
|
968
|
+
if (code === 13 || code === 10) {
|
|
969
|
+
cleanup();
|
|
970
|
+
resolve(value.trim());
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
if (code === 127 || code === 8) {
|
|
974
|
+
value = value.slice(0, -1);
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
value += char;
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
stdin.on("data", onData);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
/** Parse `1,3,5`, `all`, or `none` into 0-based task indices. */
|
|
984
|
+
export function parseCommaSeparatedTaskSelection(input, count) {
|
|
985
|
+
const trimmed = input.trim().toLowerCase();
|
|
986
|
+
if (!trimmed)
|
|
987
|
+
return null;
|
|
988
|
+
if (trimmed === "all" || trimmed === "*") {
|
|
989
|
+
return "all";
|
|
990
|
+
}
|
|
991
|
+
if (trimmed === "none" || trimmed === "-") {
|
|
992
|
+
return "none";
|
|
993
|
+
}
|
|
994
|
+
const indices = [];
|
|
995
|
+
for (const part of trimmed.split(/[,;\s]+/)) {
|
|
996
|
+
const n = parseInt(part.trim(), 10);
|
|
997
|
+
if (Number.isNaN(n) || n < 1 || n > count) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
if (!indices.includes(n - 1)) {
|
|
1001
|
+
indices.push(n - 1);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return indices.length > 0 ? indices : null;
|
|
1005
|
+
}
|
|
1006
|
+
export function modelForTaskAssignment(task, provider, model) {
|
|
1007
|
+
return task === "structuring" ? getStructuringModel(provider, model) : model;
|
|
1008
|
+
}
|
|
837
1009
|
/**
|
|
838
1010
|
* Format a price for display: "$0.80" or "free".
|
|
839
1011
|
*/
|
|
@@ -926,7 +1098,7 @@ async function loadExistingConfig(projectDir) {
|
|
|
926
1098
|
* Returns the provider name or "skip".
|
|
927
1099
|
* If currentProvider is given, shows it as the current value.
|
|
928
1100
|
*/
|
|
929
|
-
async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
1101
|
+
export async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
930
1102
|
const currentHint = currentProvider ? ` ${DIM}(current: ${currentProvider})${RESET}` : "";
|
|
931
1103
|
const providerOptions = PROVIDER_ORDER.map((key) => {
|
|
932
1104
|
const tiers = dynamicModels[key] ?? PROVIDER_TIERS[key];
|
|
@@ -947,7 +1119,7 @@ async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
|
947
1119
|
* Returns the model string. Includes a "Custom (enter model name)"
|
|
948
1120
|
* option so users can type any model ID not in the curated list.
|
|
949
1121
|
*/
|
|
950
|
-
async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
1122
|
+
export async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
951
1123
|
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
952
1124
|
if (!tiers || tiers.length === 0) {
|
|
953
1125
|
// No tiers available — fall back to direct entry
|
|
@@ -971,8 +1143,10 @@ async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
|
971
1143
|
}
|
|
972
1144
|
return tiers[tierIndex].model;
|
|
973
1145
|
}
|
|
974
|
-
// ─── Main Setup Wizard ──────────────────────────────────────────────────────
|
|
975
1146
|
export async function runSetup(opts) {
|
|
1147
|
+
if (opts.section === "keys") {
|
|
1148
|
+
return runKeysSetup();
|
|
1149
|
+
}
|
|
976
1150
|
const version = getVersion();
|
|
977
1151
|
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
978
1152
|
// ─── Non-interactive mode ─────────────────────────────────────────────
|
|
@@ -1199,6 +1373,7 @@ export async function runSetup(opts) {
|
|
|
1199
1373
|
groq: "GROQ_API_KEY",
|
|
1200
1374
|
xai: "XAI_API_KEY",
|
|
1201
1375
|
mistral: "MISTRAL_API_KEY",
|
|
1376
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
1202
1377
|
};
|
|
1203
1378
|
const legacyEnvVar = legacyEnvVars[provider] ?? "";
|
|
1204
1379
|
if (needsKey) {
|
|
@@ -1373,26 +1548,27 @@ export async function runSetup(opts) {
|
|
|
1373
1548
|
console.log();
|
|
1374
1549
|
console.log(`${DIM}Testing ${provider}/${model}...${RESET}`);
|
|
1375
1550
|
try {
|
|
1376
|
-
const { validateModel } = await import("./modelValidation.js");
|
|
1377
1551
|
const customBaseUrl = provider === "custom"
|
|
1378
1552
|
? process.env.GNOSYS_LLM_BASE_URL
|
|
1379
1553
|
: undefined;
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1554
|
+
const { proceed } = await validateModelWithKeyRotation({
|
|
1555
|
+
rl,
|
|
1556
|
+
provider,
|
|
1557
|
+
model,
|
|
1558
|
+
apiKey: capturedApiKey,
|
|
1559
|
+
customBaseUrl,
|
|
1560
|
+
isLocalProvider,
|
|
1561
|
+
saveAnywayPrompt: " Continue anyway?",
|
|
1562
|
+
saveAnywayDefault: true,
|
|
1563
|
+
});
|
|
1564
|
+
if (!proceed) {
|
|
1565
|
+
console.log(` ${DIM}Setup paused. Re-run when ready: gnosys setup${RESET}`);
|
|
1566
|
+
setupCompleted = true;
|
|
1567
|
+
rl.close();
|
|
1568
|
+
return {
|
|
1569
|
+
provider, model, structuringModel: "",
|
|
1570
|
+
apiKeyWritten, ides: [], mode: "agent", upgraded,
|
|
1571
|
+
};
|
|
1396
1572
|
}
|
|
1397
1573
|
}
|
|
1398
1574
|
catch (err) {
|
|
@@ -1671,6 +1847,18 @@ export async function runSetup(opts) {
|
|
|
1671
1847
|
await updateConfig(storePath, configUpdates);
|
|
1672
1848
|
console.log();
|
|
1673
1849
|
console.log(` ${CHECK} Config written to ${storePath}/gnosys.json`);
|
|
1850
|
+
const savedCfg = await loadConfig(storePath);
|
|
1851
|
+
const keyReqs = buildApiKeyRequirementsFromConfig(savedCfg);
|
|
1852
|
+
if (keyReqs.length > 0) {
|
|
1853
|
+
await ensureApiKeys(rl, keyReqs, askInput, {
|
|
1854
|
+
warn: WARN,
|
|
1855
|
+
check: CHECK,
|
|
1856
|
+
cross: CROSS,
|
|
1857
|
+
dim: DIM,
|
|
1858
|
+
reset: RESET,
|
|
1859
|
+
maskKey,
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1674
1862
|
}
|
|
1675
1863
|
catch (err) {
|
|
1676
1864
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1840,6 +2028,166 @@ export async function runProviderOnlySetup(opts = {}) {
|
|
|
1840
2028
|
rl.close();
|
|
1841
2029
|
}
|
|
1842
2030
|
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Build API key requirements from the selected task set only (§3.7).
|
|
2033
|
+
* One global key per distinct cloud provider in the selection.
|
|
2034
|
+
*/
|
|
2035
|
+
export function buildInlineKeyRequirements(selectedTasks, providerForTask) {
|
|
2036
|
+
const providers = [
|
|
2037
|
+
...new Set(selectedTasks
|
|
2038
|
+
.map((t) => providerForTask(t))
|
|
2039
|
+
.filter((p) => providerNeedsApiKey(p))
|
|
2040
|
+
.map((p) => p)),
|
|
2041
|
+
];
|
|
2042
|
+
return providers.map((provider) => ({ provider, scope: "global" }));
|
|
2043
|
+
}
|
|
2044
|
+
/** Write or replace a single `NAME=value` line in ~/.config/gnosys/.env. */
|
|
2045
|
+
export async function writeServiceKeyToEnv(service, key) {
|
|
2046
|
+
const configDir = path.join(os.homedir(), ".config", "gnosys");
|
|
2047
|
+
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
2048
|
+
await fs.chmod(configDir, 0o700);
|
|
2049
|
+
const envPath = path.join(configDir, ".env");
|
|
2050
|
+
let lines = [];
|
|
2051
|
+
try {
|
|
2052
|
+
lines = (await fs.readFile(envPath, "utf-8")).split("\n");
|
|
2053
|
+
}
|
|
2054
|
+
catch {
|
|
2055
|
+
// fresh file
|
|
2056
|
+
}
|
|
2057
|
+
const prefix = `${service}=`;
|
|
2058
|
+
let replaced = false;
|
|
2059
|
+
lines = lines.map((line) => {
|
|
2060
|
+
if (line.startsWith(prefix)) {
|
|
2061
|
+
replaced = true;
|
|
2062
|
+
return `${service}=${key}`;
|
|
2063
|
+
}
|
|
2064
|
+
return line;
|
|
2065
|
+
});
|
|
2066
|
+
if (!replaced) {
|
|
2067
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
2068
|
+
lines.push("");
|
|
2069
|
+
}
|
|
2070
|
+
lines.push(`${service}=${key}`);
|
|
2071
|
+
}
|
|
2072
|
+
await fs.writeFile(envPath, lines.join("\n"), { mode: 0o600 });
|
|
2073
|
+
await fs.chmod(envPath, 0o600);
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Validate a provider/model/key triple without persisting the key (§3.8).
|
|
2077
|
+
* On auth failure, re-prompt for a replacement key in memory only.
|
|
2078
|
+
*/
|
|
2079
|
+
export async function validateTaskCombo(opts) {
|
|
2080
|
+
const { Spinner } = await import("./setup/ui/spinner.js");
|
|
2081
|
+
const reprompt = opts.repromptKey ??
|
|
2082
|
+
(async (rl, provider) => {
|
|
2083
|
+
console.log();
|
|
2084
|
+
console.log(` ${WARN} Your API key may be expired or invalid.`);
|
|
2085
|
+
const key = await askInput(rl, `Enter your ${provider} API key`);
|
|
2086
|
+
return key || null;
|
|
2087
|
+
});
|
|
2088
|
+
let apiKey = opts.apiKey;
|
|
2089
|
+
const validateSpin = Spinner(`validating ${opts.provider} / ${opts.model}…`);
|
|
2090
|
+
let result = await validateModel(opts.provider, opts.model, apiKey, {
|
|
2091
|
+
customBaseUrl: opts.customBaseUrl,
|
|
2092
|
+
});
|
|
2093
|
+
if (result.ok) {
|
|
2094
|
+
validateSpin.ok("model validated", `${result.latencyMs} ms · ${opts.provider} / ${opts.model}`);
|
|
2095
|
+
return { proceed: true, apiKey };
|
|
2096
|
+
}
|
|
2097
|
+
validateSpin.fail("model test failed", result.error);
|
|
2098
|
+
if (!opts.isLocalProvider && isApiKeyValidationError(result.error)) {
|
|
2099
|
+
const newKey = await reprompt(opts.rl, opts.provider);
|
|
2100
|
+
if (newKey) {
|
|
2101
|
+
apiKey = newKey;
|
|
2102
|
+
const retrySpin = Spinner(`re-validating ${opts.provider} / ${opts.model}…`);
|
|
2103
|
+
result = await validateModel(opts.provider, opts.model, apiKey, {
|
|
2104
|
+
customBaseUrl: opts.customBaseUrl,
|
|
2105
|
+
});
|
|
2106
|
+
if (result.ok) {
|
|
2107
|
+
retrySpin.ok("model validated", `${result.latencyMs} ms · ${opts.provider} / ${opts.model}`);
|
|
2108
|
+
return { proceed: true, apiKey };
|
|
2109
|
+
}
|
|
2110
|
+
retrySpin.fail("model test failed", result.error);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
const proceed = await askYesNo(opts.rl, opts.saveAnywayPrompt ?? "Save routing anyway?", opts.saveAnywayDefault ?? false);
|
|
2114
|
+
return { proceed, apiKey };
|
|
2115
|
+
}
|
|
2116
|
+
const LEGACY_PROVIDER_ENV = {
|
|
2117
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
2118
|
+
openai: "OPENAI_API_KEY",
|
|
2119
|
+
groq: "GROQ_API_KEY",
|
|
2120
|
+
xai: "XAI_API_KEY",
|
|
2121
|
+
mistral: "MISTRAL_API_KEY",
|
|
2122
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
2123
|
+
};
|
|
2124
|
+
/** Ask where to store a validated key; persist or print env-var hints (§3.10). */
|
|
2125
|
+
export async function promptKeyDestinationAndPersist(opts) {
|
|
2126
|
+
const isMac = process.platform === "darwin";
|
|
2127
|
+
const isLinux = process.platform === "linux";
|
|
2128
|
+
const hasSecret = isLinux && hasSecretTool();
|
|
2129
|
+
const options = [];
|
|
2130
|
+
if (isMac) {
|
|
2131
|
+
options.push("OS secure store (macOS Keychain) — recommended");
|
|
2132
|
+
}
|
|
2133
|
+
else if (hasSecret) {
|
|
2134
|
+
options.push("OS secure store (GNOME Keyring) — recommended");
|
|
2135
|
+
}
|
|
2136
|
+
else {
|
|
2137
|
+
options.push("OS secure store — recommended");
|
|
2138
|
+
}
|
|
2139
|
+
options.push("Config file (~/.config/gnosys/.env)");
|
|
2140
|
+
options.push("Don't store — I'll set it as an environment variable myself");
|
|
2141
|
+
const choice = opts.destinationChoice ??
|
|
2142
|
+
(await askChoice(opts.rl, "Where should I store this key?", options));
|
|
2143
|
+
const secureIdx = 0;
|
|
2144
|
+
const dotenvIdx = 1;
|
|
2145
|
+
const noneIdx = 2;
|
|
2146
|
+
if (choice === secureIdx) {
|
|
2147
|
+
if (storeApiKeySecret(opts.service, opts.key, opts.provider)) {
|
|
2148
|
+
const store = process.platform === "darwin" ? "macOS Keychain" : "GNOME Keyring";
|
|
2149
|
+
console.log(` ${CHECK} Key saved to ${store} (${maskKey(opts.key)})`);
|
|
2150
|
+
return "secure";
|
|
2151
|
+
}
|
|
2152
|
+
console.log(` ${CROSS} Failed to write to secure store. Saving to ~/.config/gnosys/.env instead.`);
|
|
2153
|
+
await writeServiceKeyToEnv(opts.service, opts.key);
|
|
2154
|
+
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(opts.key)})`);
|
|
2155
|
+
return "dotenv";
|
|
2156
|
+
}
|
|
2157
|
+
if (choice === dotenvIdx) {
|
|
2158
|
+
await writeServiceKeyToEnv(opts.service, opts.key);
|
|
2159
|
+
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(opts.key)})`);
|
|
2160
|
+
return "dotenv";
|
|
2161
|
+
}
|
|
2162
|
+
const legacy = LEGACY_PROVIDER_ENV[opts.provider];
|
|
2163
|
+
const providerSvc = apiKeyServiceName(opts.provider, "provider");
|
|
2164
|
+
console.log();
|
|
2165
|
+
console.log(` ${DIM}Set one of these environment variables:${RESET}`);
|
|
2166
|
+
console.log(` ${GREEN}export ${opts.service}=your-key-here${RESET}`);
|
|
2167
|
+
if (opts.service !== providerSvc) {
|
|
2168
|
+
console.log(` ${DIM}or provider fallback:${RESET} ${GREEN}export ${providerSvc}=…${RESET}`);
|
|
2169
|
+
}
|
|
2170
|
+
if (legacy) {
|
|
2171
|
+
console.log(` ${DIM}or legacy:${RESET} ${GREEN}export ${legacy}=…${RESET}`);
|
|
2172
|
+
}
|
|
2173
|
+
return "none";
|
|
2174
|
+
}
|
|
2175
|
+
/** Build taskModels patch — only selected routable tasks that changed. */
|
|
2176
|
+
export function buildTaskModelsPatchFromAccepted(accepted, currentByTask, selectedSet) {
|
|
2177
|
+
const patch = {};
|
|
2178
|
+
for (const task of ROUTABLE_TASK_LIST) {
|
|
2179
|
+
if (!selectedSet.has(task))
|
|
2180
|
+
continue;
|
|
2181
|
+
const next = accepted[task];
|
|
2182
|
+
if (!next)
|
|
2183
|
+
continue;
|
|
2184
|
+
const cur = currentByTask[task];
|
|
2185
|
+
if (next.provider !== cur.provider || next.model !== cur.model) {
|
|
2186
|
+
patch[task] = next;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
2190
|
+
}
|
|
1843
2191
|
/**
|
|
1844
2192
|
* Models-only configuration — prompts for provider, model, and key (or accepts
|
|
1845
2193
|
* them via options for non-interactive use). Validates the model against the
|
|
@@ -1850,7 +2198,6 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1850
2198
|
const ownsRl = !opts.rl;
|
|
1851
2199
|
const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
|
|
1852
2200
|
try {
|
|
1853
|
-
// v5.9.3 Screen 3 — Header + Title at the top.
|
|
1854
2201
|
const { Header } = await import("./setup/ui/header.js");
|
|
1855
2202
|
const { Title } = await import("./setup/ui/title.js");
|
|
1856
2203
|
const { Spinner } = await import("./setup/ui/spinner.js");
|
|
@@ -1859,16 +2206,13 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1859
2206
|
console.log();
|
|
1860
2207
|
console.log(Header(["gnosys", "setup", "models"]));
|
|
1861
2208
|
console.log();
|
|
1862
|
-
console.log(Title("Model configuration", "pick a provider
|
|
2209
|
+
console.log(Title("Model configuration", "pick a provider — then default or per-task routing"));
|
|
1863
2210
|
console.log();
|
|
1864
2211
|
const existingConfig = await loadExistingConfig(projectDir);
|
|
1865
2212
|
const currentProvider = existingConfig?.llm.defaultProvider;
|
|
1866
2213
|
const currentModel = existingConfig
|
|
1867
2214
|
? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
|
|
1868
2215
|
: undefined;
|
|
1869
|
-
// Step 1: provider (or use --provider flag). v5.9.3: animate the
|
|
1870
|
-
// OpenRouter pricing fetch under a Spinner so the user gets feedback
|
|
1871
|
-
// on what would otherwise feel like a hang.
|
|
1872
2216
|
const pricingSpin = Spinner("fetching latest pricing from openrouter…");
|
|
1873
2217
|
const fetchStart = Date.now();
|
|
1874
2218
|
const dynamicModels = await fetchDynamicModels();
|
|
@@ -1878,8 +2222,6 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1878
2222
|
pricingSpin.ok(`pricing loaded · ${modelCount} models cached`, `${fetchMs} ms`);
|
|
1879
2223
|
}
|
|
1880
2224
|
else {
|
|
1881
|
-
// No-op fallback (cache miss + network fail) — keep the hardcoded
|
|
1882
|
-
// tiers but signal that we're running offline.
|
|
1883
2225
|
pricingSpin.fail("pricing fetch failed", "using bundled tiers");
|
|
1884
2226
|
}
|
|
1885
2227
|
console.log();
|
|
@@ -1895,103 +2237,339 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1895
2237
|
else {
|
|
1896
2238
|
provider = await pickProvider(rl, dynamicModels, "Choose your LLM provider", currentProvider);
|
|
1897
2239
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2240
|
+
const storePath = ensureActiveStorePath(projectDir);
|
|
2241
|
+
const cfgBefore = existingConfig ?? (await loadConfig(storePath));
|
|
2242
|
+
const nonInteractiveDefault = !!(opts.provider || opts.model);
|
|
2243
|
+
if (nonInteractiveDefault) {
|
|
2244
|
+
await runModelsDefaultOnlySetup({
|
|
2245
|
+
rl,
|
|
2246
|
+
opts,
|
|
2247
|
+
provider,
|
|
2248
|
+
dynamicModels,
|
|
2249
|
+
storePath,
|
|
2250
|
+
existingConfig,
|
|
2251
|
+
currentProvider,
|
|
2252
|
+
currentModel,
|
|
2253
|
+
printDiff,
|
|
2254
|
+
printStatus,
|
|
2255
|
+
});
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
console.log();
|
|
2259
|
+
const intentChoice = await askChoice(rl, "What do you want to configure?", [
|
|
2260
|
+
"Default only — set default provider and model",
|
|
2261
|
+
"Assign to specific tasks — route selected tasks only (does not change default)",
|
|
2262
|
+
]);
|
|
2263
|
+
if (intentChoice === 0) {
|
|
2264
|
+
await runModelsDefaultOnlySetup({
|
|
2265
|
+
rl,
|
|
2266
|
+
opts,
|
|
2267
|
+
provider,
|
|
2268
|
+
dynamicModels,
|
|
2269
|
+
storePath,
|
|
2270
|
+
existingConfig,
|
|
2271
|
+
currentProvider,
|
|
2272
|
+
currentModel,
|
|
2273
|
+
printDiff,
|
|
2274
|
+
printStatus,
|
|
2275
|
+
});
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
await runModelsTaskRoutingSetup({
|
|
2279
|
+
rl,
|
|
2280
|
+
opts,
|
|
2281
|
+
provider,
|
|
2282
|
+
dynamicModels,
|
|
2283
|
+
storePath,
|
|
2284
|
+
cfgBefore,
|
|
2285
|
+
printStatus,
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
finally {
|
|
2289
|
+
if (ownsRl)
|
|
2290
|
+
rl.close();
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
async function resolveKeyInMemory(opts) {
|
|
2294
|
+
if (!providerNeedsApiKey(opts.provider))
|
|
2295
|
+
return "";
|
|
2296
|
+
const cached = opts.keyCache.get(opts.provider);
|
|
2297
|
+
if (cached)
|
|
2298
|
+
return cached;
|
|
2299
|
+
const stored = readStoredSecret(apiKeyServiceName(opts.provider, "global"));
|
|
2300
|
+
if (stored) {
|
|
2301
|
+
opts.keyCache.set(opts.provider, stored);
|
|
2302
|
+
return stored;
|
|
2303
|
+
}
|
|
2304
|
+
const key = await askInput(opts.rl, `API key for ${opts.provider} (shared across selected tasks)`);
|
|
2305
|
+
opts.keyCache.set(opts.provider, key);
|
|
2306
|
+
return key;
|
|
2307
|
+
}
|
|
2308
|
+
async function runModelsDefaultOnlySetup(ctx) {
|
|
2309
|
+
const { rl, opts, provider, dynamicModels, storePath, printDiff, printStatus } = ctx;
|
|
2310
|
+
const isLocalProvider = provider === "ollama" || provider === "lmstudio";
|
|
2311
|
+
let model;
|
|
2312
|
+
if (opts.model) {
|
|
2313
|
+
model = opts.model;
|
|
2314
|
+
printStatus("ok", "model", model);
|
|
2315
|
+
}
|
|
2316
|
+
else {
|
|
2317
|
+
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
2318
|
+
if (provider === "custom" || !tiers || tiers.length === 0) {
|
|
2319
|
+
model = await askInput(rl, "Model name");
|
|
1903
2320
|
}
|
|
1904
2321
|
else {
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
model = await askInput(rl, "Model name");
|
|
1908
|
-
}
|
|
1909
|
-
else {
|
|
1910
|
-
const showCurrent = currentProvider === provider ? currentModel : undefined;
|
|
1911
|
-
model = await pickModel(rl, provider, dynamicModels, "Choose model", showCurrent);
|
|
1912
|
-
}
|
|
2322
|
+
const showCurrent = ctx.currentProvider === provider ? ctx.currentModel : undefined;
|
|
2323
|
+
model = await pickModel(rl, provider, dynamicModels, "Choose model", showCurrent);
|
|
1913
2324
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
2325
|
+
}
|
|
2326
|
+
if (!model) {
|
|
2327
|
+
printStatus("fail", "no model selected · aborting");
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
let apiKey = readFirstInChain(provider) ?? "";
|
|
2331
|
+
let keyEnteredThisRun = false;
|
|
2332
|
+
if (!apiKey && !isLocalProvider) {
|
|
2333
|
+
console.log();
|
|
2334
|
+
apiKey = await askInput(rl, `API key for ${provider}`);
|
|
2335
|
+
keyEnteredThisRun = !!apiKey;
|
|
2336
|
+
if (!apiKey) {
|
|
2337
|
+
console.log(`${WARN} No API key found for ${provider}. Validation may be skipped.`);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
const shouldValidate = opts.validate !== false;
|
|
2341
|
+
if (shouldValidate && (apiKey || isLocalProvider)) {
|
|
2342
|
+
console.log();
|
|
2343
|
+
const customBaseUrl = provider === "custom" ? process.env.GNOSYS_LLM_BASE_URL : undefined;
|
|
2344
|
+
const { proceed, apiKey: validatedKey } = await validateModelWithKeyRotation({
|
|
2345
|
+
rl,
|
|
2346
|
+
provider,
|
|
2347
|
+
model,
|
|
2348
|
+
apiKey,
|
|
2349
|
+
customBaseUrl,
|
|
2350
|
+
isLocalProvider,
|
|
2351
|
+
saveAnywayPrompt: "Save config anyway?",
|
|
2352
|
+
saveAnywayDefault: false,
|
|
2353
|
+
keyScope: "provider",
|
|
2354
|
+
});
|
|
2355
|
+
apiKey = validatedKey;
|
|
2356
|
+
if (!proceed) {
|
|
2357
|
+
printStatus("warn", "cancelled · no changes written");
|
|
1916
2358
|
return;
|
|
1917
2359
|
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2360
|
+
if (keyEnteredThisRun && apiKey && !isLocalProvider) {
|
|
2361
|
+
const service = apiKeyServiceName(provider, "provider");
|
|
2362
|
+
await promptKeyDestinationAndPersist({
|
|
2363
|
+
rl,
|
|
2364
|
+
service,
|
|
2365
|
+
provider,
|
|
2366
|
+
key: apiKey,
|
|
2367
|
+
scope: "provider",
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
const existingLlm = ctx.existingConfig?.llm;
|
|
2372
|
+
const existingProviderConfig = existingLlm
|
|
2373
|
+
? existingLlm[provider]
|
|
2374
|
+
: undefined;
|
|
2375
|
+
const providerConfigBase = typeof existingProviderConfig === "object" && existingProviderConfig !== null
|
|
2376
|
+
? existingProviderConfig
|
|
2377
|
+
: {};
|
|
2378
|
+
await updateConfig(storePath, {
|
|
2379
|
+
llm: {
|
|
2380
|
+
...(existingLlm ?? {}),
|
|
2381
|
+
defaultProvider: provider,
|
|
2382
|
+
[provider]: {
|
|
2383
|
+
...providerConfigBase,
|
|
2384
|
+
model,
|
|
2385
|
+
},
|
|
2386
|
+
},
|
|
2387
|
+
});
|
|
2388
|
+
const { buildModelsDiffRows } = await import("./setup/modelsRender.js");
|
|
2389
|
+
console.log();
|
|
2390
|
+
printDiff(buildModelsDiffRows(ctx.currentProvider, ctx.currentModel, provider, model));
|
|
2391
|
+
printStatus("ok", `saved · ${storePath}/gnosys.json`);
|
|
2392
|
+
}
|
|
2393
|
+
async function runModelsTaskRoutingSetup(ctx) {
|
|
2394
|
+
const { rl, opts, provider: initialProvider, dynamicModels, storePath, cfgBefore, printStatus } = ctx;
|
|
2395
|
+
const tasks = ASSIGNABLE_TASK_LIST;
|
|
2396
|
+
const currentByTask = {};
|
|
2397
|
+
for (const task of tasks) {
|
|
2398
|
+
currentByTask[task] = getAssignableRouting(cfgBefore, task);
|
|
2399
|
+
}
|
|
2400
|
+
const dreamEnabledBefore = !!cfgBefore.dream?.enabled;
|
|
2401
|
+
console.log();
|
|
2402
|
+
console.log(`${BOLD}Which tasks should use ${initialProvider}?${RESET}`);
|
|
2403
|
+
console.log(`${DIM}Enter numbers (comma-separated), ${BOLD}all${RESET}${DIM}, or ${BOLD}none${RESET}${DIM}. Unlisted tasks keep their current routing.${RESET}`);
|
|
2404
|
+
console.log();
|
|
2405
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
2406
|
+
const task = tasks[i];
|
|
2407
|
+
const cur = currentByTask[task];
|
|
2408
|
+
const desc = TASK_DESCRIPTIONS[task] ?? "";
|
|
2409
|
+
const off = task === "dream" && !dreamEnabledBefore ? ` ${DIM}[off]${RESET}` : "";
|
|
2410
|
+
console.log(` ${BOLD}${i + 1}.${RESET} ${task.padEnd(14)} ${DIM}now:${RESET} ${cur.provider} / ${cur.model} ${DIM}(${desc})${RESET}${off}`);
|
|
2411
|
+
}
|
|
2412
|
+
console.log();
|
|
2413
|
+
let selectedIndices = [];
|
|
2414
|
+
while (true) {
|
|
2415
|
+
const raw = await askInput(rl, `Tasks for ${initialProvider} (e.g. 5,6 or all)`, { default: "all" });
|
|
2416
|
+
const parsed = parseCommaSeparatedTaskSelection(raw, tasks.length);
|
|
2417
|
+
if (parsed === "all") {
|
|
2418
|
+
selectedIndices = tasks.map((_, i) => i);
|
|
2419
|
+
break;
|
|
2420
|
+
}
|
|
2421
|
+
if (parsed === "none") {
|
|
2422
|
+
selectedIndices = [];
|
|
2423
|
+
break;
|
|
1937
2424
|
}
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
1940
|
-
|
|
2425
|
+
if (parsed && parsed.length > 0) {
|
|
2426
|
+
selectedIndices = parsed;
|
|
2427
|
+
break;
|
|
1941
2428
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2429
|
+
console.log(`${RED}Enter numbers 1-${tasks.length}, comma-separated, or 'all'.${RESET}`);
|
|
2430
|
+
}
|
|
2431
|
+
const selectedSet = new Set(selectedIndices.map((i) => tasks[i]));
|
|
2432
|
+
if (selectedSet.size === 0) {
|
|
2433
|
+
printStatus("warn", "no tasks selected · no changes written");
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const selectedTasks = [...selectedSet];
|
|
2437
|
+
const keyRequirements = buildInlineKeyRequirements(selectedTasks, () => initialProvider);
|
|
2438
|
+
if (keyRequirements.length > 0) {
|
|
2439
|
+
console.log();
|
|
2440
|
+
console.log(` ${DIM}Keys needed for:${RESET}`);
|
|
2441
|
+
for (const req of keyRequirements) {
|
|
2442
|
+
const svc = apiKeyServiceName(req.provider, req.scope);
|
|
2443
|
+
console.log(` ${DIM} all selected → ${req.provider} (${svc})${RESET}`);
|
|
2444
|
+
}
|
|
2445
|
+
console.log();
|
|
2446
|
+
}
|
|
2447
|
+
const keyCache = new Map();
|
|
2448
|
+
const persistedServices = new Set();
|
|
2449
|
+
const accepted = {};
|
|
2450
|
+
for (const task of selectedTasks) {
|
|
2451
|
+
let taskProvider = initialProvider;
|
|
2452
|
+
const cur = currentByTask[task];
|
|
2453
|
+
while (true) {
|
|
2454
|
+
const isLocal = taskProvider === "ollama" || taskProvider === "lmstudio";
|
|
2455
|
+
const showCurrent = cur.provider === taskProvider ? cur.model : undefined;
|
|
1947
2456
|
console.log();
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
if (result.ok) {
|
|
1954
|
-
validateSpin.ok("model validated", `${result.latencyMs} ms · ${provider} / ${model}`);
|
|
2457
|
+
const model = await pickModel(rl, taskProvider, dynamicModels, `Model for ${task}`, showCurrent);
|
|
2458
|
+
if (!model) {
|
|
2459
|
+
printStatus("fail", `no model for ${task} · skipping`);
|
|
2460
|
+
accepted[task] = cur;
|
|
2461
|
+
break;
|
|
1955
2462
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2463
|
+
const apiKey = await resolveKeyInMemory({
|
|
2464
|
+
rl,
|
|
2465
|
+
provider: taskProvider,
|
|
2466
|
+
keyCache,
|
|
2467
|
+
});
|
|
2468
|
+
const shouldValidate = opts.validate !== false;
|
|
2469
|
+
let proceed = true;
|
|
2470
|
+
let validatedKey = apiKey;
|
|
2471
|
+
if (shouldValidate && (apiKey || isLocal)) {
|
|
2472
|
+
const customBaseUrl = taskProvider === "custom" ? process.env.GNOSYS_LLM_BASE_URL : undefined;
|
|
2473
|
+
const result = await validateTaskCombo({
|
|
2474
|
+
rl,
|
|
2475
|
+
provider: taskProvider,
|
|
2476
|
+
model,
|
|
2477
|
+
apiKey,
|
|
2478
|
+
customBaseUrl,
|
|
2479
|
+
isLocalProvider: isLocal,
|
|
2480
|
+
saveAnywayPrompt: `Save ${task} routing anyway?`,
|
|
2481
|
+
saveAnywayDefault: false,
|
|
2482
|
+
});
|
|
2483
|
+
proceed = result.proceed;
|
|
2484
|
+
validatedKey = result.apiKey;
|
|
2485
|
+
}
|
|
2486
|
+
if (proceed) {
|
|
2487
|
+
accepted[task] = {
|
|
2488
|
+
provider: taskProvider,
|
|
2489
|
+
model,
|
|
2490
|
+
};
|
|
2491
|
+
if (validatedKey && !isLocal) {
|
|
2492
|
+
const service = apiKeyServiceName(taskProvider, "global");
|
|
2493
|
+
if (!persistedServices.has(service)) {
|
|
2494
|
+
await promptKeyDestinationAndPersist({
|
|
2495
|
+
rl,
|
|
2496
|
+
service,
|
|
2497
|
+
provider: taskProvider,
|
|
2498
|
+
key: validatedKey,
|
|
2499
|
+
scope: "global",
|
|
2500
|
+
});
|
|
2501
|
+
persistedServices.add(service);
|
|
2502
|
+
}
|
|
2503
|
+
keyCache.set(taskProvider, validatedKey);
|
|
1962
2504
|
}
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
const retryChoice = await askChoice(rl, "Validation failed. What next?", [
|
|
2508
|
+
"Pick a different provider + model",
|
|
2509
|
+
"Keep provider, pick a new model",
|
|
2510
|
+
"Skip this task (keep current routing)",
|
|
2511
|
+
]);
|
|
2512
|
+
if (retryChoice === 0) {
|
|
2513
|
+
const picked = await pickProvider(rl, dynamicModels, `Provider for ${task}`, taskProvider);
|
|
2514
|
+
if (picked)
|
|
2515
|
+
taskProvider = picked;
|
|
2516
|
+
continue;
|
|
1963
2517
|
}
|
|
2518
|
+
if (retryChoice === 1) {
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
accepted[task] = cur;
|
|
2522
|
+
break;
|
|
1964
2523
|
}
|
|
1965
|
-
// Step 5: write config (v5.9.4 Bug 10 — unified store resolution).
|
|
1966
|
-
const storePath = ensureActiveStorePath(projectDir);
|
|
1967
|
-
const existingLlm = existingConfig?.llm;
|
|
1968
|
-
const existingProviderConfig = existingLlm
|
|
1969
|
-
? existingLlm[provider]
|
|
1970
|
-
: undefined;
|
|
1971
|
-
const providerConfigBase = (typeof existingProviderConfig === "object" && existingProviderConfig !== null)
|
|
1972
|
-
? existingProviderConfig
|
|
1973
|
-
: {};
|
|
1974
|
-
await updateConfig(storePath, {
|
|
1975
|
-
llm: {
|
|
1976
|
-
...(existingLlm ?? {}),
|
|
1977
|
-
defaultProvider: provider,
|
|
1978
|
-
[provider]: {
|
|
1979
|
-
...providerConfigBase,
|
|
1980
|
-
model,
|
|
1981
|
-
},
|
|
1982
|
-
},
|
|
1983
|
-
});
|
|
1984
|
-
// v5.9.3 Screen 3 — Diff() before the saved confirmation. Shows what
|
|
1985
|
-
// landed in gnosys.json.
|
|
1986
|
-
const { buildModelsDiffRows } = await import("./setup/modelsRender.js");
|
|
1987
|
-
console.log();
|
|
1988
|
-
printDiff(buildModelsDiffRows(currentProvider, currentModel, provider, model));
|
|
1989
|
-
printStatus("ok", `saved · ${storePath}/gnosys.json`);
|
|
1990
2524
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2525
|
+
const planned = { ...currentByTask };
|
|
2526
|
+
for (const task of selectedTasks) {
|
|
2527
|
+
if (accepted[task])
|
|
2528
|
+
planned[task] = accepted[task];
|
|
2529
|
+
}
|
|
2530
|
+
console.log();
|
|
2531
|
+
console.log(`${BOLD}Planned task routing${RESET}`);
|
|
2532
|
+
console.log(` ${"Task".padEnd(16)}${"Provider / model".padEnd(42)}${RESET}`);
|
|
2533
|
+
console.log(` ${"\u2500".repeat(56)}`);
|
|
2534
|
+
for (const task of tasks) {
|
|
2535
|
+
const p = planned[task];
|
|
2536
|
+
const marker = selectedSet.has(task) ? `${CYAN}*${RESET} ` : " ";
|
|
2537
|
+
const off = task === "dream" && !dreamEnabledBefore
|
|
2538
|
+
? ` ${DIM}(dream off)${RESET}`
|
|
2539
|
+
: "";
|
|
2540
|
+
console.log(`${marker}${task.padEnd(14)}${p.provider} / ${p.model}${off}`);
|
|
2541
|
+
}
|
|
2542
|
+
console.log(`${DIM} * = changed in this run${RESET}`);
|
|
2543
|
+
console.log();
|
|
2544
|
+
const confirmed = await askYesNo(rl, "Save this routing?", true);
|
|
2545
|
+
if (!confirmed) {
|
|
2546
|
+
printStatus("warn", "cancelled · no changes written");
|
|
2547
|
+
return;
|
|
1994
2548
|
}
|
|
2549
|
+
const taskModelsPatch = buildTaskModelsPatchFromAccepted(accepted, currentByTask, selectedSet);
|
|
2550
|
+
let dreamPatch;
|
|
2551
|
+
if (selectedSet.has("dream") && accepted.dream) {
|
|
2552
|
+
dreamPatch = {
|
|
2553
|
+
...(cfgBefore.dream ?? {}),
|
|
2554
|
+
provider: accepted.dream.provider,
|
|
2555
|
+
model: accepted.dream.model,
|
|
2556
|
+
};
|
|
2557
|
+
if (!dreamEnabledBefore &&
|
|
2558
|
+
(await askYesNo(rl, "Enable dream mode with this routing?", true))) {
|
|
2559
|
+
dreamPatch.enabled = true;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
const updatePayload = {};
|
|
2563
|
+
if (taskModelsPatch)
|
|
2564
|
+
updatePayload.taskModels = taskModelsPatch;
|
|
2565
|
+
if (dreamPatch)
|
|
2566
|
+
updatePayload.dream = dreamPatch;
|
|
2567
|
+
if (!taskModelsPatch && !dreamPatch) {
|
|
2568
|
+
printStatus("warn", "no routing changes to save");
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
await updateConfig(storePath, updatePayload);
|
|
2572
|
+
printStatus("ok", `saved task routing · ${storePath}/gnosys.json`);
|
|
1995
2573
|
}
|
|
1996
2574
|
/**
|
|
1997
2575
|
* Lightweight model-management command. Supports three operations:
|
|
@@ -2143,8 +2721,12 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2143
2721
|
dream: { ...(existingDream ?? {}), enabled: false },
|
|
2144
2722
|
});
|
|
2145
2723
|
clearDreamMachineEverywhere();
|
|
2724
|
+
const { uninstallDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
2725
|
+
const launchdPath = uninstallDreamLaunchAgent();
|
|
2146
2726
|
console.log();
|
|
2147
2727
|
printStatus("ok", "dream mode disabled · designation cleared");
|
|
2728
|
+
if (launchdPath)
|
|
2729
|
+
printStatus("ok", "launchd agent removed", launchdPath);
|
|
2148
2730
|
localDb.close();
|
|
2149
2731
|
remoteDb?.close();
|
|
2150
2732
|
return;
|
|
@@ -2201,26 +2783,27 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2201
2783
|
}
|
|
2202
2784
|
// Validate — animated Spinner with the model latency reported.
|
|
2203
2785
|
if (dreamProvider !== "skip") {
|
|
2204
|
-
const apiKey = await getApiKeyForProvider(dreamProvider);
|
|
2786
|
+
const apiKey = await getApiKeyForProvider(dreamProvider, { task: "dream" });
|
|
2205
2787
|
const isLocalProvider = dreamProvider === "ollama" || dreamProvider === "lmstudio";
|
|
2206
2788
|
if (apiKey || isLocalProvider) {
|
|
2207
2789
|
console.log();
|
|
2208
|
-
const validateSpin = Spinner(`validating ${dreamProvider} / ${dreamModel}…`);
|
|
2209
2790
|
try {
|
|
2210
2791
|
const customBaseUrl = dreamProvider === "custom" ? process.env.GNOSYS_LLM_BASE_URL : undefined;
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2792
|
+
const { proceed } = await validateModelWithKeyRotation({
|
|
2793
|
+
rl,
|
|
2794
|
+
provider: dreamProvider,
|
|
2795
|
+
model: dreamModel,
|
|
2796
|
+
apiKey,
|
|
2797
|
+
customBaseUrl,
|
|
2798
|
+
isLocalProvider,
|
|
2799
|
+
saveAnywayPrompt: "save config anyway?",
|
|
2800
|
+
saveAnywayDefault: true,
|
|
2801
|
+
});
|
|
2802
|
+
if (!proceed) {
|
|
2803
|
+
printStatus("warn", "setup cancelled · no changes written");
|
|
2804
|
+
localDb.close();
|
|
2805
|
+
remoteDb?.close();
|
|
2806
|
+
return;
|
|
2224
2807
|
}
|
|
2225
2808
|
}
|
|
2226
2809
|
catch (err) {
|
|
@@ -2228,7 +2811,7 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2228
2811
|
}
|
|
2229
2812
|
}
|
|
2230
2813
|
else {
|
|
2231
|
-
printStatus("warn", `no API key for ${dreamProvider}`, "set one via `gnosys setup
|
|
2814
|
+
printStatus("warn", `no API key for ${dreamProvider}`, "set one via `gnosys setup providers`");
|
|
2232
2815
|
}
|
|
2233
2816
|
}
|
|
2234
2817
|
// ─── 7.2 Thresholds + sub-tasks ───────────────────────────────────
|
|
@@ -2241,6 +2824,12 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2241
2824
|
const dIdle = existingDream?.idleMinutes ?? 10;
|
|
2242
2825
|
const dRuntime = existingDream?.maxRuntimeMinutes ?? 30;
|
|
2243
2826
|
const dMinMem = existingDream?.minMemories ?? 10;
|
|
2827
|
+
const dScheduleStart = existingDream?.schedule?.startHour ?? 2;
|
|
2828
|
+
const dScheduleEnd = existingDream?.schedule?.endHour ?? 5;
|
|
2829
|
+
const dSystemIdle = existingDream?.systemIdleMinutes ?? 30;
|
|
2830
|
+
const dMinNewMemories = existingDream?.minNewMemoriesToDream ?? 10;
|
|
2831
|
+
const dMinHours = existingDream?.minHoursBetweenRuns ?? 20;
|
|
2832
|
+
const dMaxCalls = existingDream?.maxLLMCallsPerRun ?? 12;
|
|
2244
2833
|
const dSelfCritique = existingDream?.selfCritique ?? true;
|
|
2245
2834
|
const dGenSummaries = existingDream?.generateSummaries ?? true;
|
|
2246
2835
|
const dDiscover = existingDream?.discoverRelationships ?? true;
|
|
@@ -2257,6 +2846,12 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2257
2846
|
let idleMinutes = dIdle;
|
|
2258
2847
|
let maxRuntimeMinutes = dRuntime;
|
|
2259
2848
|
let minMemories = dMinMem;
|
|
2849
|
+
let scheduleStartHour = dScheduleStart;
|
|
2850
|
+
let scheduleEndHour = dScheduleEnd;
|
|
2851
|
+
let systemIdleMinutes = dSystemIdle;
|
|
2852
|
+
let minNewMemoriesToDream = dMinNewMemories;
|
|
2853
|
+
let minHoursBetweenRuns = dMinHours;
|
|
2854
|
+
let maxLLMCallsPerRun = dMaxCalls;
|
|
2260
2855
|
let selfCritique = dSelfCritique;
|
|
2261
2856
|
let generateSummaries = dGenSummaries;
|
|
2262
2857
|
let discoverRelationships = dDiscover;
|
|
@@ -2267,6 +2862,18 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2267
2862
|
maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns) || dRuntime);
|
|
2268
2863
|
const minMemAns = await askInput(rl, "minimum memories before activating", { default: String(dMinMem) });
|
|
2269
2864
|
minMemories = Math.max(1, parseInt(minMemAns) || dMinMem);
|
|
2865
|
+
const scheduleStartAns = await askInput(rl, "night window start hour (0-23)", { default: String(dScheduleStart) });
|
|
2866
|
+
scheduleStartHour = Math.min(23, Math.max(0, parseInt(scheduleStartAns) || dScheduleStart));
|
|
2867
|
+
const scheduleEndAns = await askInput(rl, "night window end hour (0-23)", { default: String(dScheduleEnd) });
|
|
2868
|
+
scheduleEndHour = Math.min(23, Math.max(0, parseInt(scheduleEndAns) || dScheduleEnd));
|
|
2869
|
+
const systemIdleAns = await askInput(rl, "real machine idle minutes required", { default: String(dSystemIdle) });
|
|
2870
|
+
systemIdleMinutes = Math.max(1, parseInt(systemIdleAns) || dSystemIdle);
|
|
2871
|
+
const minNewAns = await askInput(rl, "minimum changed memories before dreaming", { default: String(dMinNewMemories) });
|
|
2872
|
+
minNewMemoriesToDream = Math.max(0, parseInt(minNewAns) || dMinNewMemories);
|
|
2873
|
+
const minHoursAns = await askInput(rl, "minimum hours between successful dreams", { default: String(dMinHours) });
|
|
2874
|
+
minHoursBetweenRuns = Math.max(0, parseInt(minHoursAns) || dMinHours);
|
|
2875
|
+
const maxCallsAns = await askInput(rl, "maximum LLM calls per dream run", { default: String(dMaxCalls) });
|
|
2876
|
+
maxLLMCallsPerRun = Math.max(0, parseInt(maxCallsAns) || dMaxCalls);
|
|
2270
2877
|
selfCritique = await askYesNo(rl, "self-critique (rule + LLM-based review flagging)", dSelfCritique);
|
|
2271
2878
|
generateSummaries = await askYesNo(rl, "generate summaries (LLM)", dGenSummaries);
|
|
2272
2879
|
discoverRelationships = await askYesNo(rl, "discover relationships (LLM)", dDiscover);
|
|
@@ -2281,6 +2888,11 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2281
2888
|
minMemories,
|
|
2282
2889
|
provider: dreamProvider,
|
|
2283
2890
|
model: dreamModel || undefined,
|
|
2891
|
+
schedule: { startHour: scheduleStartHour, endHour: scheduleEndHour },
|
|
2892
|
+
systemIdleMinutes,
|
|
2893
|
+
minNewMemoriesToDream,
|
|
2894
|
+
minHoursBetweenRuns,
|
|
2895
|
+
maxLLMCallsPerRun,
|
|
2284
2896
|
selfCritique,
|
|
2285
2897
|
generateSummaries,
|
|
2286
2898
|
discoverRelationships,
|
|
@@ -2289,6 +2901,8 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2289
2901
|
// Reset consecutive failure counter on a fresh setup so Layer 4
|
|
2290
2902
|
// doesn't fire immediately based on stale history.
|
|
2291
2903
|
localDb.resetDreamConsecutiveFailures();
|
|
2904
|
+
const { installDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
2905
|
+
const launchdPath = installDreamLaunchAgent();
|
|
2292
2906
|
localDb.close();
|
|
2293
2907
|
remoteDb?.close();
|
|
2294
2908
|
// Final Diff block per the design — provider/machine + the two
|
|
@@ -2316,7 +2930,9 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2316
2930
|
discoverRelationships,
|
|
2317
2931
|
}));
|
|
2318
2932
|
const dreamerName = designate ? localMachine : (designatedMachine ?? "the designated machine");
|
|
2319
|
-
printStatus("progress", `
|
|
2933
|
+
printStatus("progress", `scheduled dream checks run nightly (${scheduleStartHour}:00-${scheduleEndHour}:00) on ${dreamerName}`);
|
|
2934
|
+
if (launchdPath)
|
|
2935
|
+
printStatus("ok", "launchd agent installed", launchdPath);
|
|
2320
2936
|
printStatus("progress", "check status anytime with `gnosys status --system`");
|
|
2321
2937
|
}
|
|
2322
2938
|
finally {
|
|
@@ -2379,29 +2995,18 @@ async function openRemoteDbIfConfigured(localDb) {
|
|
|
2379
2995
|
* Best-effort lookup of the API key for a provider. Used by the dream setup
|
|
2380
2996
|
* wizard to power the validation step. Mirrors the resolveApiKey precedence.
|
|
2381
2997
|
*/
|
|
2382
|
-
export async function getApiKeyForProvider(provider) {
|
|
2383
|
-
if (provider === "ollama" || provider === "lmstudio" || provider === "skip")
|
|
2998
|
+
export async function getApiKeyForProvider(provider, opts) {
|
|
2999
|
+
if (provider === "ollama" || provider === "lmstudio" || provider === "skip") {
|
|
2384
3000
|
return "";
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
};
|
|
2393
|
-
const fromEnv = process.env[envVarName] || (legacyVars[provider] && process.env[legacyVars[provider]]) || "";
|
|
2394
|
-
if (fromEnv)
|
|
2395
|
-
return fromEnv;
|
|
2396
|
-
if (process.platform === "darwin") {
|
|
2397
|
-
try {
|
|
2398
|
-
return execSync(`security find-generic-password -a "$USER" -s "${envVarName}" -w 2>/dev/null`, {
|
|
2399
|
-
stdio: "pipe", encoding: "utf-8", timeout: 2000,
|
|
2400
|
-
}).trim();
|
|
2401
|
-
}
|
|
2402
|
-
catch {
|
|
2403
|
-
// fall through
|
|
3001
|
+
}
|
|
3002
|
+
if (opts?.directory) {
|
|
3003
|
+
const cfg = await loadExistingConfig(opts.directory);
|
|
3004
|
+
if (cfg) {
|
|
3005
|
+
const fromCfg = getApiKeyForProviderFromConfig(cfg, provider);
|
|
3006
|
+
if (fromCfg)
|
|
3007
|
+
return fromCfg;
|
|
2404
3008
|
}
|
|
2405
3009
|
}
|
|
2406
|
-
return "";
|
|
3010
|
+
return readFirstInChain(provider) ?? "";
|
|
2407
3011
|
}
|
|
3012
|
+
export { getApiKeyForProviderFromConfig } from "./apiKeyVault.js";
|