gnosys 5.11.4 → 5.12.2
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 +377 -5162
- package/dist/index.js +542 -244
- package/dist/lib/addCommand.d.ts +9 -0
- package/dist/lib/addCommand.js +102 -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/archive.js +0 -2
- package/dist/lib/askCommand.d.ts +13 -0
- package/dist/lib/askCommand.js +145 -0
- package/dist/lib/attachCommand.d.ts +17 -0
- package/dist/lib/attachCommand.js +66 -0
- package/dist/lib/attachments.d.ts +43 -2
- package/dist/lib/attachments.js +81 -2
- 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/chat/choose.js +2 -2
- 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 +76 -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 +41 -48
- 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 +68 -1
- package/dist/lib/db.js +385 -120
- package/dist/lib/dbWrite.d.ts +1 -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/docxExtract.js +1 -1
- package/dist/lib/dream.d.ts +50 -2
- package/dist/lib/dream.js +324 -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 +234 -0
- package/dist/lib/embeddings.js +3 -3
- package/dist/lib/exportCommand.d.ts +18 -0
- package/dist/lib/exportCommand.js +101 -0
- package/dist/lib/exportProject.d.ts +3 -2
- package/dist/lib/exportProject.js +2 -1
- package/dist/lib/federated.js +1 -1
- 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/importProject.js +2 -1
- 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 +27 -9
- package/dist/lib/localDiskCheck.d.ts +17 -0
- package/dist/lib/localDiskCheck.js +54 -0
- package/dist/lib/lock.d.ts +1 -1
- package/dist/lib/lock.js +5 -3
- 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/migrate.js +0 -1
- 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/multimodalIngest.js +1 -1
- package/dist/lib/openrouterTiers.d.ts +29 -0
- package/dist/lib/openrouterTiers.js +113 -0
- package/dist/lib/platform.d.ts +0 -6
- package/dist/lib/platform.js +0 -28
- 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 +63 -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/rulesGen.d.ts +8 -0
- package/dist/lib/rulesGen.js +16 -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/search.d.ts +0 -2
- package/dist/lib/search.js +0 -7
- 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 +307 -0
- package/dist/lib/setup/sections/routing.d.ts +2 -6
- package/dist/lib/setup/sections/routing.js +67 -82
- package/dist/lib/setup/sections/taskRoutingEditor.d.ts +13 -0
- package/dist/lib/setup/sections/taskRoutingEditor.js +139 -0
- package/dist/lib/setup/summary.d.ts +9 -0
- package/dist/lib/setup/summary.js +51 -37
- package/dist/lib/setup/ui/header.js +0 -1
- package/dist/lib/setup.d.ts +105 -15
- package/dist/lib/setup.js +747 -287
- 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 +41 -0
- package/dist/lib/syncClient.js +234 -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 +30 -0
- package/dist/lib/syncIngest.js +175 -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 +32 -0
- package/dist/lib/syncSnapshot.js +188 -0
- package/dist/lib/syncStaging.d.ts +79 -0
- package/dist/lib/syncStaging.js +237 -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/webIndex.js +0 -1
- 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/dist/sandbox/client.js +1 -1
- package/dist/sandbox/manager.js +1 -14
- package/dist/sandbox/server.js +3 -5
- package/package.json +6 -2
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 });
|
|
@@ -265,16 +271,6 @@ async function fetchDynamicModels() {
|
|
|
265
271
|
return {};
|
|
266
272
|
}
|
|
267
273
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Get model tiers for a provider — tries dynamic first, falls back to hardcoded.
|
|
270
|
-
*/
|
|
271
|
-
async function getModelTiers(provider) {
|
|
272
|
-
const dynamic = await fetchDynamicModels();
|
|
273
|
-
if (dynamic[provider] && dynamic[provider].length > 0) {
|
|
274
|
-
return dynamic[provider];
|
|
275
|
-
}
|
|
276
|
-
return PROVIDER_TIERS[provider] ?? [];
|
|
277
|
-
}
|
|
278
274
|
// ─── Provider display names and env var mapping ─────────────────────────────
|
|
279
275
|
const PROVIDER_DISPLAY = {
|
|
280
276
|
anthropic: "Anthropic (Claude)",
|
|
@@ -284,6 +280,7 @@ const PROVIDER_DISPLAY = {
|
|
|
284
280
|
xai: "xAI (Grok)",
|
|
285
281
|
mistral: "Mistral",
|
|
286
282
|
lmstudio: "LM Studio (local, free)",
|
|
283
|
+
openrouter: "OpenRouter (free + paid models)",
|
|
287
284
|
custom: "Custom (any OpenAI-compatible API)",
|
|
288
285
|
};
|
|
289
286
|
const PROVIDER_ENV_VAR = {
|
|
@@ -292,6 +289,7 @@ const PROVIDER_ENV_VAR = {
|
|
|
292
289
|
groq: "GNOSYS_GROQ_KEY",
|
|
293
290
|
xai: "GNOSYS_XAI_KEY",
|
|
294
291
|
mistral: "GNOSYS_MISTRAL_KEY",
|
|
292
|
+
openrouter: "GNOSYS_OPENROUTER_KEY",
|
|
295
293
|
custom: "GNOSYS_CUSTOM_KEY",
|
|
296
294
|
};
|
|
297
295
|
// Ordered list for the menu
|
|
@@ -303,16 +301,40 @@ const PROVIDER_ORDER = [
|
|
|
303
301
|
"xai",
|
|
304
302
|
"mistral",
|
|
305
303
|
"lmstudio",
|
|
304
|
+
"openrouter",
|
|
306
305
|
"custom",
|
|
307
306
|
];
|
|
308
307
|
// Task descriptions for display
|
|
309
|
-
const TASK_DESCRIPTIONS = {
|
|
308
|
+
export const TASK_DESCRIPTIONS = {
|
|
310
309
|
structuring: "adding memories, tagging",
|
|
311
310
|
synthesis: "Q&A answers",
|
|
311
|
+
chat: "interactive chat TUI",
|
|
312
312
|
vision: "images, PDFs",
|
|
313
313
|
transcription: "audio files",
|
|
314
314
|
dream: "overnight consolidation",
|
|
315
315
|
};
|
|
316
|
+
/** Tasks routed via taskModels in gnosys.json */
|
|
317
|
+
const ROUTABLE_TASK_LIST = [
|
|
318
|
+
"structuring",
|
|
319
|
+
"synthesis",
|
|
320
|
+
"vision",
|
|
321
|
+
"transcription",
|
|
322
|
+
"chat",
|
|
323
|
+
];
|
|
324
|
+
export const ASSIGNABLE_TASK_LIST = [
|
|
325
|
+
...ROUTABLE_TASK_LIST,
|
|
326
|
+
"dream",
|
|
327
|
+
];
|
|
328
|
+
export function getAssignableRouting(cfg, task) {
|
|
329
|
+
if (task === "dream") {
|
|
330
|
+
const dreamProvider = (cfg.dream?.provider ?? "ollama");
|
|
331
|
+
return {
|
|
332
|
+
provider: dreamProvider,
|
|
333
|
+
model: cfg.dream?.model ?? getProviderModel(cfg, dreamProvider),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return resolveTaskModel(cfg, task);
|
|
337
|
+
}
|
|
316
338
|
// ─── Exported Helpers ───────────────────────────────────────────────────────
|
|
317
339
|
/**
|
|
318
340
|
* Returns the cheapest capable model for structuring tasks.
|
|
@@ -334,8 +356,10 @@ export function getStructuringModel(provider, chosenModel) {
|
|
|
334
356
|
* Creates the directory and file if they don't exist.
|
|
335
357
|
* Replaces an existing key line if found, otherwise appends.
|
|
336
358
|
*/
|
|
337
|
-
export async function writeApiKey(provider, key) {
|
|
338
|
-
const envVar =
|
|
359
|
+
export async function writeApiKey(provider, key, opts) {
|
|
360
|
+
const envVar = opts?.scope === "global"
|
|
361
|
+
? apiKeyServiceName(provider, "global")
|
|
362
|
+
: PROVIDER_ENV_VAR[provider];
|
|
339
363
|
if (!envVar)
|
|
340
364
|
return;
|
|
341
365
|
const configDir = path.join(os.homedir(), ".config", "gnosys");
|
|
@@ -418,6 +442,68 @@ function hasSecretTool() {
|
|
|
418
442
|
return false;
|
|
419
443
|
}
|
|
420
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Prompt for a replacement API key after validation fails, storing it in the
|
|
447
|
+
* platform secure store when available (Keychain / GNOME Keyring), else .env.
|
|
448
|
+
*/
|
|
449
|
+
async function promptAndStoreProviderApiKey(rl, provider, scope = "provider") {
|
|
450
|
+
if (provider === "ollama" || provider === "lmstudio" || provider === "skip") {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
const service = apiKeyServiceName(provider, scope);
|
|
454
|
+
console.log();
|
|
455
|
+
console.log(` ${WARN} Your API key may be expired or invalid.`);
|
|
456
|
+
console.log(` ${DIM}Enter a new key — saved as ${service}${RESET}`);
|
|
457
|
+
console.log();
|
|
458
|
+
const key = await askInput(rl, `Enter your ${provider} API key`);
|
|
459
|
+
if (!key) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
if (storeApiKeySecret(service, key, provider)) {
|
|
463
|
+
const store = process.platform === "darwin" ? "macOS Keychain" : "GNOME Keyring";
|
|
464
|
+
console.log(` ${CHECK} Key saved to ${store} (${maskKey(key)})`);
|
|
465
|
+
return key;
|
|
466
|
+
}
|
|
467
|
+
console.log(` ${CROSS} Failed to write to secure store. Saving to ~/.config/gnosys/.env instead.`);
|
|
468
|
+
await writeApiKey(provider, key);
|
|
469
|
+
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
|
|
470
|
+
return key;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Run model validation; on API-key auth failures, offer to rotate the key and
|
|
474
|
+
* retry once before asking whether to save config anyway.
|
|
475
|
+
*/
|
|
476
|
+
async function validateModelWithKeyRotation(opts) {
|
|
477
|
+
const { Spinner } = await import("./setup/ui/spinner.js");
|
|
478
|
+
let apiKey = opts.apiKey;
|
|
479
|
+
const validateSpin = Spinner(`validating ${opts.provider} / ${opts.model}…`);
|
|
480
|
+
let result = await validateModel(opts.provider, opts.model, apiKey, {
|
|
481
|
+
customBaseUrl: opts.customBaseUrl,
|
|
482
|
+
});
|
|
483
|
+
if (result.ok) {
|
|
484
|
+
validateSpin.ok("model validated", `${result.latencyMs} ms · ${opts.provider} / ${opts.model}`);
|
|
485
|
+
return { proceed: true, apiKey };
|
|
486
|
+
}
|
|
487
|
+
validateSpin.fail("model test failed", result.error);
|
|
488
|
+
if (!opts.isLocalProvider &&
|
|
489
|
+
isApiKeyValidationError(result.error)) {
|
|
490
|
+
const newKey = await promptAndStoreProviderApiKey(opts.rl, opts.provider, opts.keyScope ?? "global");
|
|
491
|
+
if (newKey) {
|
|
492
|
+
apiKey = newKey;
|
|
493
|
+
const retrySpin = Spinner(`re-validating ${opts.provider} / ${opts.model}…`);
|
|
494
|
+
result = await validateModel(opts.provider, opts.model, apiKey, {
|
|
495
|
+
customBaseUrl: opts.customBaseUrl,
|
|
496
|
+
});
|
|
497
|
+
if (result.ok) {
|
|
498
|
+
retrySpin.ok("model validated", `${result.latencyMs} ms · ${opts.provider} / ${opts.model}`);
|
|
499
|
+
return { proceed: true, apiKey };
|
|
500
|
+
}
|
|
501
|
+
retrySpin.fail("model test failed", result.error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const proceed = await askYesNo(opts.rl, opts.saveAnywayPrompt, opts.saveAnywayDefault);
|
|
505
|
+
return { proceed, apiKey };
|
|
506
|
+
}
|
|
421
507
|
/**
|
|
422
508
|
* Detect where an existing API key is stored.
|
|
423
509
|
* Returns description like "macOS Keychain", "env var", ".env file", or empty string.
|
|
@@ -579,7 +665,7 @@ export async function detectIDEs(projectDir) {
|
|
|
579
665
|
*/
|
|
580
666
|
export function upsertGrokMcpBlock(existing, name, entry) {
|
|
581
667
|
// Drop mistaken v5.9.4 `[mcp.<name>]` sections so we don't leave dead config.
|
|
582
|
-
|
|
668
|
+
const content = removeTomlSection(existing, `[mcp.${name}]`);
|
|
583
669
|
const sectionHeader = `[mcp_servers.${name}]`;
|
|
584
670
|
const lines = content.split("\n");
|
|
585
671
|
const headerIdx = lines.findIndex((line) => line.trim() === sectionHeader);
|
|
@@ -683,7 +769,7 @@ export async function setupIDE(ide, projectDir) {
|
|
|
683
769
|
// 1. Strip legacy hand-written sections in ~/.codex/config.toml.
|
|
684
770
|
const userCodexConfig = path.join(os.homedir(), ".codex", "config.toml");
|
|
685
771
|
try {
|
|
686
|
-
|
|
772
|
+
const existing = await fs.readFile(userCodexConfig, "utf-8");
|
|
687
773
|
const cleaned = removeTomlSection(removeTomlSection(existing, "[mcp.gnosys]"), "[gnosys]");
|
|
688
774
|
if (cleaned !== existing) {
|
|
689
775
|
await fs.writeFile(userCodexConfig, cleaned, "utf-8");
|
|
@@ -700,7 +786,7 @@ export async function setupIDE(ide, projectDir) {
|
|
|
700
786
|
let alreadyCorrect = false;
|
|
701
787
|
try {
|
|
702
788
|
const existing = runCli("codex", ["mcp", "get", "gnosys"], { allowFailure: true });
|
|
703
|
-
if (existing
|
|
789
|
+
if (existing?.includes(gnosysCmd) && !existing.includes(" serve")) {
|
|
704
790
|
alreadyCorrect = true;
|
|
705
791
|
}
|
|
706
792
|
else if (existing) {
|
|
@@ -817,16 +903,20 @@ async function askChoice(rl, question, options) {
|
|
|
817
903
|
/**
|
|
818
904
|
* Read a single line of input with an optional default value.
|
|
819
905
|
*/
|
|
820
|
-
async function askInput(rl, prompt, opts) {
|
|
906
|
+
export async function askInput(rl, prompt, opts) {
|
|
821
907
|
const suffix = opts?.default ? ` ${DIM}(${opts.default})${RESET}` : "";
|
|
822
908
|
const answer = await safeQuestion(rl, `${prompt}${suffix}: `);
|
|
823
909
|
const trimmed = answer.trim();
|
|
824
910
|
return trimmed || opts?.default || "";
|
|
825
911
|
}
|
|
912
|
+
export function printInfo(text, meta) {
|
|
913
|
+
const suffix = meta ? ` ${DIM}${meta}${RESET}` : "";
|
|
914
|
+
console.log(` ${text}${suffix}`);
|
|
915
|
+
}
|
|
826
916
|
/**
|
|
827
917
|
* Y/n prompt. Returns true for yes.
|
|
828
918
|
*/
|
|
829
|
-
async function askYesNo(rl, question, defaultYes = true) {
|
|
919
|
+
export async function askYesNo(rl, question, defaultYes = true) {
|
|
830
920
|
const hint = defaultYes ? "Y/n" : "y/N";
|
|
831
921
|
const answer = await safeQuestion(rl, `${question} [${hint}] `);
|
|
832
922
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -834,6 +924,78 @@ async function askYesNo(rl, question, defaultYes = true) {
|
|
|
834
924
|
return defaultYes;
|
|
835
925
|
return trimmed === "y" || trimmed === "yes";
|
|
836
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* Read a password/API key without echoing it back to the terminal.
|
|
929
|
+
*/
|
|
930
|
+
export async function askPassword(rl, prompt) {
|
|
931
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
|
|
932
|
+
return askInput(rl, prompt);
|
|
933
|
+
}
|
|
934
|
+
return new Promise((resolve) => {
|
|
935
|
+
let value = "";
|
|
936
|
+
stdout.write(`${prompt}: `);
|
|
937
|
+
stdin.setRawMode(true);
|
|
938
|
+
stdin.resume();
|
|
939
|
+
const cleanup = () => {
|
|
940
|
+
stdin.setRawMode(false);
|
|
941
|
+
stdin.off("data", onData);
|
|
942
|
+
stdout.write("\n");
|
|
943
|
+
};
|
|
944
|
+
const onData = (chunk) => {
|
|
945
|
+
const input = chunk.toString("utf-8");
|
|
946
|
+
for (const char of input) {
|
|
947
|
+
const code = char.charCodeAt(0);
|
|
948
|
+
if (code === 3) {
|
|
949
|
+
cleanup();
|
|
950
|
+
try {
|
|
951
|
+
rl.close();
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
// already closed
|
|
955
|
+
}
|
|
956
|
+
process.exit(130);
|
|
957
|
+
}
|
|
958
|
+
if (code === 13 || code === 10) {
|
|
959
|
+
cleanup();
|
|
960
|
+
resolve(value.trim());
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (code === 127 || code === 8) {
|
|
964
|
+
value = value.slice(0, -1);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
value += char;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
stdin.on("data", onData);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
/** Parse `1,3,5`, `all`, or `none` into 0-based task indices. */
|
|
974
|
+
export function parseCommaSeparatedTaskSelection(input, count) {
|
|
975
|
+
const trimmed = input.trim().toLowerCase();
|
|
976
|
+
if (!trimmed)
|
|
977
|
+
return null;
|
|
978
|
+
if (trimmed === "all" || trimmed === "*") {
|
|
979
|
+
return "all";
|
|
980
|
+
}
|
|
981
|
+
if (trimmed === "none" || trimmed === "-") {
|
|
982
|
+
return "none";
|
|
983
|
+
}
|
|
984
|
+
const indices = [];
|
|
985
|
+
for (const part of trimmed.split(/[,;\s]+/)) {
|
|
986
|
+
const n = parseInt(part.trim(), 10);
|
|
987
|
+
if (Number.isNaN(n) || n < 1 || n > count) {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
if (!indices.includes(n - 1)) {
|
|
991
|
+
indices.push(n - 1);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return indices.length > 0 ? indices : null;
|
|
995
|
+
}
|
|
996
|
+
export function modelForTaskAssignment(task, provider, model) {
|
|
997
|
+
return task === "structuring" ? getStructuringModel(provider, model) : model;
|
|
998
|
+
}
|
|
837
999
|
/**
|
|
838
1000
|
* Format a price for display: "$0.80" or "free".
|
|
839
1001
|
*/
|
|
@@ -926,7 +1088,7 @@ async function loadExistingConfig(projectDir) {
|
|
|
926
1088
|
* Returns the provider name or "skip".
|
|
927
1089
|
* If currentProvider is given, shows it as the current value.
|
|
928
1090
|
*/
|
|
929
|
-
async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
1091
|
+
export async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
930
1092
|
const currentHint = currentProvider ? ` ${DIM}(current: ${currentProvider})${RESET}` : "";
|
|
931
1093
|
const providerOptions = PROVIDER_ORDER.map((key) => {
|
|
932
1094
|
const tiers = dynamicModels[key] ?? PROVIDER_TIERS[key];
|
|
@@ -947,7 +1109,7 @@ async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
|
947
1109
|
* Returns the model string. Includes a "Custom (enter model name)"
|
|
948
1110
|
* option so users can type any model ID not in the curated list.
|
|
949
1111
|
*/
|
|
950
|
-
async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
1112
|
+
export async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
951
1113
|
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
952
1114
|
if (!tiers || tiers.length === 0) {
|
|
953
1115
|
// No tiers available — fall back to direct entry
|
|
@@ -971,8 +1133,10 @@ async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
|
971
1133
|
}
|
|
972
1134
|
return tiers[tierIndex].model;
|
|
973
1135
|
}
|
|
974
|
-
// ─── Main Setup Wizard ──────────────────────────────────────────────────────
|
|
975
1136
|
export async function runSetup(opts) {
|
|
1137
|
+
if (opts.section === "keys") {
|
|
1138
|
+
return runKeysSetup();
|
|
1139
|
+
}
|
|
976
1140
|
const version = getVersion();
|
|
977
1141
|
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
978
1142
|
// ─── Non-interactive mode ─────────────────────────────────────────────
|
|
@@ -1199,6 +1363,7 @@ export async function runSetup(opts) {
|
|
|
1199
1363
|
groq: "GROQ_API_KEY",
|
|
1200
1364
|
xai: "XAI_API_KEY",
|
|
1201
1365
|
mistral: "MISTRAL_API_KEY",
|
|
1366
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
1202
1367
|
};
|
|
1203
1368
|
const legacyEnvVar = legacyEnvVars[provider] ?? "";
|
|
1204
1369
|
if (needsKey) {
|
|
@@ -1264,8 +1429,7 @@ export async function runSetup(opts) {
|
|
|
1264
1429
|
const gnomeIdx = hasSecret ? idx++ : -1;
|
|
1265
1430
|
const envIdx = idx++;
|
|
1266
1431
|
const dotenvIdx = idx++;
|
|
1267
|
-
|
|
1268
|
-
// skipIdx is unused as a variable but documents the last index
|
|
1432
|
+
idx++; // skip entry consumes the last index (value itself unused)
|
|
1269
1433
|
if (keyChoice === keychainIdx) {
|
|
1270
1434
|
// macOS Keychain — reuse existing key if available, otherwise ask
|
|
1271
1435
|
console.log();
|
|
@@ -1373,26 +1537,27 @@ export async function runSetup(opts) {
|
|
|
1373
1537
|
console.log();
|
|
1374
1538
|
console.log(`${DIM}Testing ${provider}/${model}...${RESET}`);
|
|
1375
1539
|
try {
|
|
1376
|
-
const { validateModel } = await import("./modelValidation.js");
|
|
1377
1540
|
const customBaseUrl = provider === "custom"
|
|
1378
1541
|
? process.env.GNOSYS_LLM_BASE_URL
|
|
1379
1542
|
: undefined;
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1543
|
+
const { proceed } = await validateModelWithKeyRotation({
|
|
1544
|
+
rl,
|
|
1545
|
+
provider,
|
|
1546
|
+
model,
|
|
1547
|
+
apiKey: capturedApiKey,
|
|
1548
|
+
customBaseUrl,
|
|
1549
|
+
isLocalProvider,
|
|
1550
|
+
saveAnywayPrompt: " Continue anyway?",
|
|
1551
|
+
saveAnywayDefault: true,
|
|
1552
|
+
});
|
|
1553
|
+
if (!proceed) {
|
|
1554
|
+
console.log(` ${DIM}Setup paused. Re-run when ready: gnosys setup${RESET}`);
|
|
1555
|
+
setupCompleted = true;
|
|
1556
|
+
rl.close();
|
|
1557
|
+
return {
|
|
1558
|
+
provider, model, structuringModel: "",
|
|
1559
|
+
apiKeyWritten, ides: [], mode: "agent", upgraded,
|
|
1560
|
+
};
|
|
1396
1561
|
}
|
|
1397
1562
|
}
|
|
1398
1563
|
catch (err) {
|
|
@@ -1671,6 +1836,18 @@ export async function runSetup(opts) {
|
|
|
1671
1836
|
await updateConfig(storePath, configUpdates);
|
|
1672
1837
|
console.log();
|
|
1673
1838
|
console.log(` ${CHECK} Config written to ${storePath}/gnosys.json`);
|
|
1839
|
+
const savedCfg = await loadConfig(storePath);
|
|
1840
|
+
const keyReqs = buildApiKeyRequirementsFromConfig(savedCfg);
|
|
1841
|
+
if (keyReqs.length > 0) {
|
|
1842
|
+
await ensureApiKeys(rl, keyReqs, askInput, {
|
|
1843
|
+
warn: WARN,
|
|
1844
|
+
check: CHECK,
|
|
1845
|
+
cross: CROSS,
|
|
1846
|
+
dim: DIM,
|
|
1847
|
+
reset: RESET,
|
|
1848
|
+
maskKey,
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1674
1851
|
}
|
|
1675
1852
|
catch (err) {
|
|
1676
1853
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1787,58 +1964,163 @@ export async function runSetup(opts) {
|
|
|
1787
1964
|
}
|
|
1788
1965
|
}
|
|
1789
1966
|
/**
|
|
1790
|
-
*
|
|
1791
|
-
*
|
|
1792
|
-
* model picker — that's row 2's job.
|
|
1793
|
-
*
|
|
1794
|
-
* v5.9.4 Bug 4 — before this split, both summary rows routed through
|
|
1795
|
-
* `runModelsSetup`, leaving no way to swap provider without also choosing
|
|
1796
|
-
* a new model. Now row 1 picks a provider, row 2 picks a model.
|
|
1967
|
+
* Build API key requirements from the selected task set only (§3.7).
|
|
1968
|
+
* One global key per distinct cloud provider in the selection.
|
|
1797
1969
|
*/
|
|
1798
|
-
export
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
1970
|
+
export function buildInlineKeyRequirements(selectedTasks, providerForTask) {
|
|
1971
|
+
const providers = [
|
|
1972
|
+
...new Set(selectedTasks
|
|
1973
|
+
.map((t) => providerForTask(t))
|
|
1974
|
+
.filter((p) => providerNeedsApiKey(p))
|
|
1975
|
+
.map((p) => p)),
|
|
1976
|
+
];
|
|
1977
|
+
return providers.map((provider) => ({ provider, scope: "global" }));
|
|
1978
|
+
}
|
|
1979
|
+
/** Write or replace a single `NAME=value` line in ~/.config/gnosys/.env. */
|
|
1980
|
+
export async function writeServiceKeyToEnv(service, key) {
|
|
1981
|
+
const configDir = path.join(os.homedir(), ".config", "gnosys");
|
|
1982
|
+
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
1983
|
+
await fs.chmod(configDir, 0o700);
|
|
1984
|
+
const envPath = path.join(configDir, ".env");
|
|
1985
|
+
let lines = [];
|
|
1802
1986
|
try {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
const pricingSpin = Spinner("fetching latest pricing from openrouter…");
|
|
1815
|
-
const fetchStart = Date.now();
|
|
1816
|
-
const dynamicModels = await fetchDynamicModels();
|
|
1817
|
-
const fetchMs = Date.now() - fetchStart;
|
|
1818
|
-
if (Object.keys(dynamicModels).length > 0) {
|
|
1819
|
-
pricingSpin.ok("pricing loaded", `${fetchMs} ms`);
|
|
1820
|
-
}
|
|
1821
|
-
else {
|
|
1822
|
-
pricingSpin.fail("pricing fetch failed", "using bundled tiers");
|
|
1987
|
+
lines = (await fs.readFile(envPath, "utf-8")).split("\n");
|
|
1988
|
+
}
|
|
1989
|
+
catch {
|
|
1990
|
+
// fresh file
|
|
1991
|
+
}
|
|
1992
|
+
const prefix = `${service}=`;
|
|
1993
|
+
let replaced = false;
|
|
1994
|
+
lines = lines.map((line) => {
|
|
1995
|
+
if (line.startsWith(prefix)) {
|
|
1996
|
+
replaced = true;
|
|
1997
|
+
return `${service}=${key}`;
|
|
1823
1998
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1999
|
+
return line;
|
|
2000
|
+
});
|
|
2001
|
+
if (!replaced) {
|
|
2002
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") {
|
|
2003
|
+
lines.push("");
|
|
1829
2004
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2005
|
+
lines.push(`${service}=${key}`);
|
|
2006
|
+
}
|
|
2007
|
+
await fs.writeFile(envPath, lines.join("\n"), { mode: 0o600 });
|
|
2008
|
+
await fs.chmod(envPath, 0o600);
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Validate a provider/model/key triple without persisting the key (§3.8).
|
|
2012
|
+
* On auth failure, re-prompt for a replacement key in memory only.
|
|
2013
|
+
*/
|
|
2014
|
+
export async function validateTaskCombo(opts) {
|
|
2015
|
+
const { Spinner } = await import("./setup/ui/spinner.js");
|
|
2016
|
+
const reprompt = opts.repromptKey ??
|
|
2017
|
+
(async (rl, provider) => {
|
|
2018
|
+
console.log();
|
|
2019
|
+
console.log(` ${WARN} Your API key may be expired or invalid.`);
|
|
2020
|
+
const key = await askInput(rl, `Enter your ${provider} API key`);
|
|
2021
|
+
return key || null;
|
|
1834
2022
|
});
|
|
1835
|
-
|
|
1836
|
-
|
|
2023
|
+
let apiKey = opts.apiKey;
|
|
2024
|
+
const validateSpin = Spinner(`validating ${opts.provider} / ${opts.model}…`);
|
|
2025
|
+
let result = await validateModel(opts.provider, opts.model, apiKey, {
|
|
2026
|
+
customBaseUrl: opts.customBaseUrl,
|
|
2027
|
+
});
|
|
2028
|
+
if (result.ok) {
|
|
2029
|
+
validateSpin.ok("model validated", `${result.latencyMs} ms · ${opts.provider} / ${opts.model}`);
|
|
2030
|
+
return { proceed: true, apiKey };
|
|
2031
|
+
}
|
|
2032
|
+
validateSpin.fail("model test failed", result.error);
|
|
2033
|
+
if (!opts.isLocalProvider && isApiKeyValidationError(result.error)) {
|
|
2034
|
+
const newKey = await reprompt(opts.rl, opts.provider);
|
|
2035
|
+
if (newKey) {
|
|
2036
|
+
apiKey = newKey;
|
|
2037
|
+
const retrySpin = Spinner(`re-validating ${opts.provider} / ${opts.model}…`);
|
|
2038
|
+
result = await validateModel(opts.provider, opts.model, apiKey, {
|
|
2039
|
+
customBaseUrl: opts.customBaseUrl,
|
|
2040
|
+
});
|
|
2041
|
+
if (result.ok) {
|
|
2042
|
+
retrySpin.ok("model validated", `${result.latencyMs} ms · ${opts.provider} / ${opts.model}`);
|
|
2043
|
+
return { proceed: true, apiKey };
|
|
2044
|
+
}
|
|
2045
|
+
retrySpin.fail("model test failed", result.error);
|
|
2046
|
+
}
|
|
1837
2047
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2048
|
+
const proceed = await askYesNo(opts.rl, opts.saveAnywayPrompt ?? "Save routing anyway?", opts.saveAnywayDefault ?? false);
|
|
2049
|
+
return { proceed, apiKey };
|
|
2050
|
+
}
|
|
2051
|
+
const LEGACY_PROVIDER_ENV = {
|
|
2052
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
2053
|
+
openai: "OPENAI_API_KEY",
|
|
2054
|
+
groq: "GROQ_API_KEY",
|
|
2055
|
+
xai: "XAI_API_KEY",
|
|
2056
|
+
mistral: "MISTRAL_API_KEY",
|
|
2057
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
2058
|
+
};
|
|
2059
|
+
/** Ask where to store a validated key; persist or print env-var hints (§3.10). */
|
|
2060
|
+
export async function promptKeyDestinationAndPersist(opts) {
|
|
2061
|
+
const isMac = process.platform === "darwin";
|
|
2062
|
+
const isLinux = process.platform === "linux";
|
|
2063
|
+
const hasSecret = isLinux && hasSecretTool();
|
|
2064
|
+
const options = [];
|
|
2065
|
+
if (isMac) {
|
|
2066
|
+
options.push("OS secure store (macOS Keychain) — recommended");
|
|
2067
|
+
}
|
|
2068
|
+
else if (hasSecret) {
|
|
2069
|
+
options.push("OS secure store (GNOME Keyring) — recommended");
|
|
2070
|
+
}
|
|
2071
|
+
else {
|
|
2072
|
+
options.push("OS secure store — recommended");
|
|
2073
|
+
}
|
|
2074
|
+
options.push("Config file (~/.config/gnosys/.env)");
|
|
2075
|
+
options.push("Don't store — I'll set it as an environment variable myself");
|
|
2076
|
+
const choice = opts.destinationChoice ??
|
|
2077
|
+
(await askChoice(opts.rl, "Where should I store this key?", options));
|
|
2078
|
+
const secureIdx = 0;
|
|
2079
|
+
const dotenvIdx = 1;
|
|
2080
|
+
if (choice === secureIdx) {
|
|
2081
|
+
if (storeApiKeySecret(opts.service, opts.key, opts.provider)) {
|
|
2082
|
+
const store = process.platform === "darwin" ? "macOS Keychain" : "GNOME Keyring";
|
|
2083
|
+
console.log(` ${CHECK} Key saved to ${store} (${maskKey(opts.key)})`);
|
|
2084
|
+
return "secure";
|
|
2085
|
+
}
|
|
2086
|
+
console.log(` ${CROSS} Failed to write to secure store. Saving to ~/.config/gnosys/.env instead.`);
|
|
2087
|
+
await writeServiceKeyToEnv(opts.service, opts.key);
|
|
2088
|
+
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(opts.key)})`);
|
|
2089
|
+
return "dotenv";
|
|
2090
|
+
}
|
|
2091
|
+
if (choice === dotenvIdx) {
|
|
2092
|
+
await writeServiceKeyToEnv(opts.service, opts.key);
|
|
2093
|
+
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(opts.key)})`);
|
|
2094
|
+
return "dotenv";
|
|
2095
|
+
}
|
|
2096
|
+
const legacy = LEGACY_PROVIDER_ENV[opts.provider];
|
|
2097
|
+
const providerSvc = apiKeyServiceName(opts.provider, "provider");
|
|
2098
|
+
console.log();
|
|
2099
|
+
console.log(` ${DIM}Set one of these environment variables:${RESET}`);
|
|
2100
|
+
console.log(` ${GREEN}export ${opts.service}=your-key-here${RESET}`);
|
|
2101
|
+
if (opts.service !== providerSvc) {
|
|
2102
|
+
console.log(` ${DIM}or provider fallback:${RESET} ${GREEN}export ${providerSvc}=…${RESET}`);
|
|
2103
|
+
}
|
|
2104
|
+
if (legacy) {
|
|
2105
|
+
console.log(` ${DIM}or legacy:${RESET} ${GREEN}export ${legacy}=…${RESET}`);
|
|
1841
2106
|
}
|
|
2107
|
+
return "none";
|
|
2108
|
+
}
|
|
2109
|
+
/** Build taskModels patch — only selected routable tasks that changed. */
|
|
2110
|
+
export function buildTaskModelsPatchFromAccepted(accepted, currentByTask, selectedSet) {
|
|
2111
|
+
const patch = {};
|
|
2112
|
+
for (const task of ROUTABLE_TASK_LIST) {
|
|
2113
|
+
if (!selectedSet.has(task))
|
|
2114
|
+
continue;
|
|
2115
|
+
const next = accepted[task];
|
|
2116
|
+
if (!next)
|
|
2117
|
+
continue;
|
|
2118
|
+
const cur = currentByTask[task];
|
|
2119
|
+
if (next.provider !== cur.provider || next.model !== cur.model) {
|
|
2120
|
+
patch[task] = next;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
1842
2124
|
}
|
|
1843
2125
|
/**
|
|
1844
2126
|
* Models-only configuration — prompts for provider, model, and key (or accepts
|
|
@@ -1850,7 +2132,6 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1850
2132
|
const ownsRl = !opts.rl;
|
|
1851
2133
|
const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
|
|
1852
2134
|
try {
|
|
1853
|
-
// v5.9.3 Screen 3 — Header + Title at the top.
|
|
1854
2135
|
const { Header } = await import("./setup/ui/header.js");
|
|
1855
2136
|
const { Title } = await import("./setup/ui/title.js");
|
|
1856
2137
|
const { Spinner } = await import("./setup/ui/spinner.js");
|
|
@@ -1859,16 +2140,13 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1859
2140
|
console.log();
|
|
1860
2141
|
console.log(Header(["gnosys", "setup", "models"]));
|
|
1861
2142
|
console.log();
|
|
1862
|
-
console.log(Title("Model configuration", "pick a provider
|
|
2143
|
+
console.log(Title("Model configuration", "pick a provider — then default or per-task routing"));
|
|
1863
2144
|
console.log();
|
|
1864
2145
|
const existingConfig = await loadExistingConfig(projectDir);
|
|
1865
2146
|
const currentProvider = existingConfig?.llm.defaultProvider;
|
|
1866
2147
|
const currentModel = existingConfig
|
|
1867
2148
|
? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
|
|
1868
2149
|
: 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
2150
|
const pricingSpin = Spinner("fetching latest pricing from openrouter…");
|
|
1873
2151
|
const fetchStart = Date.now();
|
|
1874
2152
|
const dynamicModels = await fetchDynamicModels();
|
|
@@ -1878,8 +2156,6 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1878
2156
|
pricingSpin.ok(`pricing loaded · ${modelCount} models cached`, `${fetchMs} ms`);
|
|
1879
2157
|
}
|
|
1880
2158
|
else {
|
|
1881
|
-
// No-op fallback (cache miss + network fail) — keep the hardcoded
|
|
1882
|
-
// tiers but signal that we're running offline.
|
|
1883
2159
|
pricingSpin.fail("pricing fetch failed", "using bundled tiers");
|
|
1884
2160
|
}
|
|
1885
2161
|
console.log();
|
|
@@ -1895,181 +2171,339 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1895
2171
|
else {
|
|
1896
2172
|
provider = await pickProvider(rl, dynamicModels, "Choose your LLM provider", currentProvider);
|
|
1897
2173
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
if (!model) {
|
|
1915
|
-
printStatus("fail", "no model selected · aborting");
|
|
2174
|
+
const storePath = ensureActiveStorePath(projectDir);
|
|
2175
|
+
const cfgBefore = existingConfig ?? (await loadConfig(storePath));
|
|
2176
|
+
const nonInteractiveDefault = !!(opts.provider || opts.model);
|
|
2177
|
+
if (nonInteractiveDefault) {
|
|
2178
|
+
await runModelsDefaultOnlySetup({
|
|
2179
|
+
rl,
|
|
2180
|
+
opts,
|
|
2181
|
+
provider,
|
|
2182
|
+
dynamicModels,
|
|
2183
|
+
storePath,
|
|
2184
|
+
existingConfig,
|
|
2185
|
+
currentProvider,
|
|
2186
|
+
currentModel,
|
|
2187
|
+
printDiff,
|
|
2188
|
+
printStatus,
|
|
2189
|
+
});
|
|
1916
2190
|
return;
|
|
1917
2191
|
}
|
|
1918
|
-
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
}
|
|
1938
|
-
if (!apiKey && provider !== "ollama" && provider !== "lmstudio") {
|
|
1939
|
-
console.log(`${WARN} No API key found for ${provider}. Run 'gnosys setup' to configure one.`);
|
|
1940
|
-
// Continue anyway — user might just want to update the model in config
|
|
1941
|
-
}
|
|
1942
|
-
// Step 4: validate (default: true) — v5.9.3 Screen 3: animated
|
|
1943
|
-
// Spinner with latency reported on success.
|
|
1944
|
-
const shouldValidate = opts.validate !== false;
|
|
1945
|
-
const isLocalProvider = provider === "ollama" || provider === "lmstudio";
|
|
1946
|
-
if (shouldValidate && (apiKey || isLocalProvider)) {
|
|
1947
|
-
console.log();
|
|
1948
|
-
const validateSpin = Spinner(`validating ${provider} / ${model}…`);
|
|
1949
|
-
const customBaseUrl = provider === "custom"
|
|
1950
|
-
? process.env.GNOSYS_LLM_BASE_URL
|
|
1951
|
-
: undefined;
|
|
1952
|
-
const result = await validateModel(provider, model, apiKey, { customBaseUrl });
|
|
1953
|
-
if (result.ok) {
|
|
1954
|
-
validateSpin.ok("model validated", `${result.latencyMs} ms · ${provider} / ${model}`);
|
|
1955
|
-
}
|
|
1956
|
-
else {
|
|
1957
|
-
validateSpin.fail("model test failed", result.error);
|
|
1958
|
-
const proceed = await askYesNo(rl, "Save config anyway?", false);
|
|
1959
|
-
if (!proceed) {
|
|
1960
|
-
printStatus("warn", "cancelled · no changes written");
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
2192
|
+
console.log();
|
|
2193
|
+
const intentChoice = await askChoice(rl, "What do you want to configure?", [
|
|
2194
|
+
"Default only — set default provider and model",
|
|
2195
|
+
"Assign to specific tasks — route selected tasks only (does not change default)",
|
|
2196
|
+
]);
|
|
2197
|
+
if (intentChoice === 0) {
|
|
2198
|
+
await runModelsDefaultOnlySetup({
|
|
2199
|
+
rl,
|
|
2200
|
+
opts,
|
|
2201
|
+
provider,
|
|
2202
|
+
dynamicModels,
|
|
2203
|
+
storePath,
|
|
2204
|
+
existingConfig,
|
|
2205
|
+
currentProvider,
|
|
2206
|
+
currentModel,
|
|
2207
|
+
printDiff,
|
|
2208
|
+
printStatus,
|
|
2209
|
+
});
|
|
2210
|
+
return;
|
|
1964
2211
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
: {};
|
|
1974
|
-
await updateConfig(storePath, {
|
|
1975
|
-
llm: {
|
|
1976
|
-
...(existingLlm ?? {}),
|
|
1977
|
-
defaultProvider: provider,
|
|
1978
|
-
[provider]: {
|
|
1979
|
-
...providerConfigBase,
|
|
1980
|
-
model,
|
|
1981
|
-
},
|
|
1982
|
-
},
|
|
2212
|
+
await runModelsTaskRoutingSetup({
|
|
2213
|
+
rl,
|
|
2214
|
+
opts,
|
|
2215
|
+
provider,
|
|
2216
|
+
dynamicModels,
|
|
2217
|
+
storePath,
|
|
2218
|
+
cfgBefore,
|
|
2219
|
+
printStatus,
|
|
1983
2220
|
});
|
|
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
2221
|
}
|
|
1991
2222
|
finally {
|
|
1992
2223
|
if (ownsRl)
|
|
1993
2224
|
rl.close();
|
|
1994
2225
|
}
|
|
1995
2226
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2227
|
+
async function resolveKeyInMemory(opts) {
|
|
2228
|
+
if (!providerNeedsApiKey(opts.provider))
|
|
2229
|
+
return "";
|
|
2230
|
+
const cached = opts.keyCache.get(opts.provider);
|
|
2231
|
+
if (cached)
|
|
2232
|
+
return cached;
|
|
2233
|
+
const stored = readStoredSecret(apiKeyServiceName(opts.provider, "global"));
|
|
2234
|
+
if (stored) {
|
|
2235
|
+
opts.keyCache.set(opts.provider, stored);
|
|
2236
|
+
return stored;
|
|
2237
|
+
}
|
|
2238
|
+
const key = await askInput(opts.rl, `API key for ${opts.provider} (shared across selected tasks)`);
|
|
2239
|
+
opts.keyCache.set(opts.provider, key);
|
|
2240
|
+
return key;
|
|
2241
|
+
}
|
|
2242
|
+
async function runModelsDefaultOnlySetup(ctx) {
|
|
2243
|
+
const { rl, opts, provider, dynamicModels, storePath, printDiff, printStatus } = ctx;
|
|
2244
|
+
const isLocalProvider = provider === "ollama" || provider === "lmstudio";
|
|
2245
|
+
let model;
|
|
2246
|
+
if (opts.model) {
|
|
2247
|
+
model = opts.model;
|
|
2248
|
+
printStatus("ok", "model", model);
|
|
2249
|
+
}
|
|
2250
|
+
else {
|
|
2251
|
+
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
2252
|
+
if (provider === "custom" || !tiers || tiers.length === 0) {
|
|
2253
|
+
model = await askInput(rl, "Model name");
|
|
2011
2254
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2255
|
+
else {
|
|
2256
|
+
const showCurrent = ctx.currentProvider === provider ? ctx.currentModel : undefined;
|
|
2257
|
+
model = await pickModel(rl, provider, dynamicModels, "Choose model", showCurrent);
|
|
2014
2258
|
}
|
|
2015
2259
|
}
|
|
2016
|
-
if (
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2260
|
+
if (!model) {
|
|
2261
|
+
printStatus("fail", "no model selected · aborting");
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
let apiKey = readFirstInChain(provider) ?? "";
|
|
2265
|
+
let keyEnteredThisRun = false;
|
|
2266
|
+
if (!apiKey && !isLocalProvider) {
|
|
2021
2267
|
console.log();
|
|
2022
|
-
|
|
2268
|
+
apiKey = await askInput(rl, `API key for ${provider}`);
|
|
2269
|
+
keyEnteredThisRun = !!apiKey;
|
|
2270
|
+
if (!apiKey) {
|
|
2271
|
+
console.log(`${WARN} No API key found for ${provider}. Validation may be skipped.`);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
const shouldValidate = opts.validate !== false;
|
|
2275
|
+
if (shouldValidate && (apiKey || isLocalProvider)) {
|
|
2023
2276
|
console.log();
|
|
2024
|
-
const
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2027
|
-
|
|
2277
|
+
const customBaseUrl = provider === "custom" ? process.env.GNOSYS_LLM_BASE_URL : undefined;
|
|
2278
|
+
const { proceed, apiKey: validatedKey } = await validateModelWithKeyRotation({
|
|
2279
|
+
rl,
|
|
2280
|
+
provider,
|
|
2281
|
+
model,
|
|
2282
|
+
apiKey,
|
|
2283
|
+
customBaseUrl,
|
|
2284
|
+
isLocalProvider,
|
|
2285
|
+
saveAnywayPrompt: "Save config anyway?",
|
|
2286
|
+
saveAnywayDefault: false,
|
|
2287
|
+
keyScope: "provider",
|
|
2288
|
+
});
|
|
2289
|
+
apiKey = validatedKey;
|
|
2290
|
+
if (!proceed) {
|
|
2291
|
+
printStatus("warn", "cancelled · no changes written");
|
|
2028
2292
|
return;
|
|
2029
2293
|
}
|
|
2030
|
-
|
|
2031
|
-
const
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2294
|
+
if (keyEnteredThisRun && apiKey && !isLocalProvider) {
|
|
2295
|
+
const service = apiKeyServiceName(provider, "provider");
|
|
2296
|
+
await promptKeyDestinationAndPersist({
|
|
2297
|
+
rl,
|
|
2298
|
+
service,
|
|
2299
|
+
provider,
|
|
2300
|
+
key: apiKey,
|
|
2301
|
+
scope: "provider",
|
|
2302
|
+
});
|
|
2036
2303
|
}
|
|
2037
|
-
return;
|
|
2038
2304
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
:
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
defaultProvider: currentProvider,
|
|
2054
|
-
[currentProvider]: { ...providerConfigBase, model: opts.set },
|
|
2305
|
+
const existingLlm = ctx.existingConfig?.llm;
|
|
2306
|
+
const existingProviderConfig = existingLlm
|
|
2307
|
+
? existingLlm[provider]
|
|
2308
|
+
: undefined;
|
|
2309
|
+
const providerConfigBase = typeof existingProviderConfig === "object" && existingProviderConfig !== null
|
|
2310
|
+
? existingProviderConfig
|
|
2311
|
+
: {};
|
|
2312
|
+
await updateConfig(storePath, {
|
|
2313
|
+
llm: {
|
|
2314
|
+
...(existingLlm ?? {}),
|
|
2315
|
+
defaultProvider: provider,
|
|
2316
|
+
[provider]: {
|
|
2317
|
+
...providerConfigBase,
|
|
2318
|
+
model,
|
|
2055
2319
|
},
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2320
|
+
},
|
|
2321
|
+
});
|
|
2322
|
+
const { buildModelsDiffRows } = await import("./setup/modelsRender.js");
|
|
2323
|
+
console.log();
|
|
2324
|
+
printDiff(buildModelsDiffRows(ctx.currentProvider, ctx.currentModel, provider, model));
|
|
2325
|
+
printStatus("ok", `saved · ${storePath}/gnosys.json`);
|
|
2326
|
+
}
|
|
2327
|
+
async function runModelsTaskRoutingSetup(ctx) {
|
|
2328
|
+
const { rl, opts, provider: initialProvider, dynamicModels, storePath, cfgBefore, printStatus } = ctx;
|
|
2329
|
+
const tasks = ASSIGNABLE_TASK_LIST;
|
|
2330
|
+
const currentByTask = {};
|
|
2331
|
+
for (const task of tasks) {
|
|
2332
|
+
currentByTask[task] = getAssignableRouting(cfgBefore, task);
|
|
2333
|
+
}
|
|
2334
|
+
const dreamEnabledBefore = !!cfgBefore.dream?.enabled;
|
|
2335
|
+
console.log();
|
|
2336
|
+
console.log(`${BOLD}Which tasks should use ${initialProvider}?${RESET}`);
|
|
2337
|
+
console.log(`${DIM}Enter numbers (comma-separated), ${BOLD}all${RESET}${DIM}, or ${BOLD}none${RESET}${DIM}. Unlisted tasks keep their current routing.${RESET}`);
|
|
2338
|
+
console.log();
|
|
2339
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
2340
|
+
const task = tasks[i];
|
|
2341
|
+
const cur = currentByTask[task];
|
|
2342
|
+
const desc = TASK_DESCRIPTIONS[task] ?? "";
|
|
2343
|
+
const off = task === "dream" && !dreamEnabledBefore ? ` ${DIM}[off]${RESET}` : "";
|
|
2344
|
+
console.log(` ${BOLD}${i + 1}.${RESET} ${task.padEnd(14)} ${DIM}now:${RESET} ${cur.provider} / ${cur.model} ${DIM}(${desc})${RESET}${off}`);
|
|
2345
|
+
}
|
|
2346
|
+
console.log();
|
|
2347
|
+
let selectedIndices = [];
|
|
2348
|
+
while (true) {
|
|
2349
|
+
const raw = await askInput(rl, `Tasks for ${initialProvider} (e.g. 5,6 or all)`, { default: "all" });
|
|
2350
|
+
const parsed = parseCommaSeparatedTaskSelection(raw, tasks.length);
|
|
2351
|
+
if (parsed === "all") {
|
|
2352
|
+
selectedIndices = tasks.map((_, i) => i);
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
if (parsed === "none") {
|
|
2356
|
+
selectedIndices = [];
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
if (parsed && parsed.length > 0) {
|
|
2360
|
+
selectedIndices = parsed;
|
|
2361
|
+
break;
|
|
2362
|
+
}
|
|
2363
|
+
console.log(`${RED}Enter numbers 1-${tasks.length}, comma-separated, or 'all'.${RESET}`);
|
|
2059
2364
|
}
|
|
2060
|
-
|
|
2061
|
-
if (
|
|
2062
|
-
|
|
2365
|
+
const selectedSet = new Set(selectedIndices.map((i) => tasks[i]));
|
|
2366
|
+
if (selectedSet.size === 0) {
|
|
2367
|
+
printStatus("warn", "no tasks selected · no changes written");
|
|
2063
2368
|
return;
|
|
2064
2369
|
}
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2067
|
-
|
|
2370
|
+
const selectedTasks = [...selectedSet];
|
|
2371
|
+
const keyRequirements = buildInlineKeyRequirements(selectedTasks, () => initialProvider);
|
|
2372
|
+
if (keyRequirements.length > 0) {
|
|
2373
|
+
console.log();
|
|
2374
|
+
console.log(` ${DIM}Keys needed for:${RESET}`);
|
|
2375
|
+
for (const req of keyRequirements) {
|
|
2376
|
+
const svc = apiKeyServiceName(req.provider, req.scope);
|
|
2377
|
+
console.log(` ${DIM} all selected → ${req.provider} (${svc})${RESET}`);
|
|
2378
|
+
}
|
|
2379
|
+
console.log();
|
|
2380
|
+
}
|
|
2381
|
+
const keyCache = new Map();
|
|
2382
|
+
const persistedServices = new Set();
|
|
2383
|
+
const accepted = {};
|
|
2384
|
+
for (const task of selectedTasks) {
|
|
2385
|
+
let taskProvider = initialProvider;
|
|
2386
|
+
const cur = currentByTask[task];
|
|
2387
|
+
while (true) {
|
|
2388
|
+
const isLocal = taskProvider === "ollama" || taskProvider === "lmstudio";
|
|
2389
|
+
const showCurrent = cur.provider === taskProvider ? cur.model : undefined;
|
|
2390
|
+
console.log();
|
|
2391
|
+
const model = await pickModel(rl, taskProvider, dynamicModels, `Model for ${task}`, showCurrent);
|
|
2392
|
+
if (!model) {
|
|
2393
|
+
printStatus("fail", `no model for ${task} · skipping`);
|
|
2394
|
+
accepted[task] = cur;
|
|
2395
|
+
break;
|
|
2396
|
+
}
|
|
2397
|
+
const apiKey = await resolveKeyInMemory({
|
|
2398
|
+
rl,
|
|
2399
|
+
provider: taskProvider,
|
|
2400
|
+
keyCache,
|
|
2401
|
+
});
|
|
2402
|
+
const shouldValidate = opts.validate !== false;
|
|
2403
|
+
let proceed = true;
|
|
2404
|
+
let validatedKey = apiKey;
|
|
2405
|
+
if (shouldValidate && (apiKey || isLocal)) {
|
|
2406
|
+
const customBaseUrl = taskProvider === "custom" ? process.env.GNOSYS_LLM_BASE_URL : undefined;
|
|
2407
|
+
const result = await validateTaskCombo({
|
|
2408
|
+
rl,
|
|
2409
|
+
provider: taskProvider,
|
|
2410
|
+
model,
|
|
2411
|
+
apiKey,
|
|
2412
|
+
customBaseUrl,
|
|
2413
|
+
isLocalProvider: isLocal,
|
|
2414
|
+
saveAnywayPrompt: `Save ${task} routing anyway?`,
|
|
2415
|
+
saveAnywayDefault: false,
|
|
2416
|
+
});
|
|
2417
|
+
proceed = result.proceed;
|
|
2418
|
+
validatedKey = result.apiKey;
|
|
2419
|
+
}
|
|
2420
|
+
if (proceed) {
|
|
2421
|
+
accepted[task] = {
|
|
2422
|
+
provider: taskProvider,
|
|
2423
|
+
model,
|
|
2424
|
+
};
|
|
2425
|
+
if (validatedKey && !isLocal) {
|
|
2426
|
+
const service = apiKeyServiceName(taskProvider, "global");
|
|
2427
|
+
if (!persistedServices.has(service)) {
|
|
2428
|
+
await promptKeyDestinationAndPersist({
|
|
2429
|
+
rl,
|
|
2430
|
+
service,
|
|
2431
|
+
provider: taskProvider,
|
|
2432
|
+
key: validatedKey,
|
|
2433
|
+
scope: "global",
|
|
2434
|
+
});
|
|
2435
|
+
persistedServices.add(service);
|
|
2436
|
+
}
|
|
2437
|
+
keyCache.set(taskProvider, validatedKey);
|
|
2438
|
+
}
|
|
2439
|
+
break;
|
|
2440
|
+
}
|
|
2441
|
+
const retryChoice = await askChoice(rl, "Validation failed. What next?", [
|
|
2442
|
+
"Pick a different provider + model",
|
|
2443
|
+
"Keep provider, pick a new model",
|
|
2444
|
+
"Skip this task (keep current routing)",
|
|
2445
|
+
]);
|
|
2446
|
+
if (retryChoice === 0) {
|
|
2447
|
+
const picked = await pickProvider(rl, dynamicModels, `Provider for ${task}`, taskProvider);
|
|
2448
|
+
if (picked)
|
|
2449
|
+
taskProvider = picked;
|
|
2450
|
+
continue;
|
|
2451
|
+
}
|
|
2452
|
+
if (retryChoice === 1) {
|
|
2453
|
+
continue;
|
|
2454
|
+
}
|
|
2455
|
+
accepted[task] = cur;
|
|
2456
|
+
break;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
const planned = { ...currentByTask };
|
|
2460
|
+
for (const task of selectedTasks) {
|
|
2461
|
+
if (accepted[task])
|
|
2462
|
+
planned[task] = accepted[task];
|
|
2463
|
+
}
|
|
2068
2464
|
console.log();
|
|
2069
|
-
console.log(
|
|
2070
|
-
console.log(`
|
|
2465
|
+
console.log(`${BOLD}Planned task routing${RESET}`);
|
|
2466
|
+
console.log(` ${"Task".padEnd(16)}${"Provider / model".padEnd(42)}${RESET}`);
|
|
2467
|
+
console.log(` ${"\u2500".repeat(56)}`);
|
|
2468
|
+
for (const task of tasks) {
|
|
2469
|
+
const p = planned[task];
|
|
2470
|
+
const marker = selectedSet.has(task) ? `${CYAN}*${RESET} ` : " ";
|
|
2471
|
+
const off = task === "dream" && !dreamEnabledBefore
|
|
2472
|
+
? ` ${DIM}(dream off)${RESET}`
|
|
2473
|
+
: "";
|
|
2474
|
+
console.log(`${marker}${task.padEnd(14)}${p.provider} / ${p.model}${off}`);
|
|
2475
|
+
}
|
|
2476
|
+
console.log(`${DIM} * = changed in this run${RESET}`);
|
|
2071
2477
|
console.log();
|
|
2072
|
-
|
|
2478
|
+
const confirmed = await askYesNo(rl, "Save this routing?", true);
|
|
2479
|
+
if (!confirmed) {
|
|
2480
|
+
printStatus("warn", "cancelled · no changes written");
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
const taskModelsPatch = buildTaskModelsPatchFromAccepted(accepted, currentByTask, selectedSet);
|
|
2484
|
+
let dreamPatch;
|
|
2485
|
+
if (selectedSet.has("dream") && accepted.dream) {
|
|
2486
|
+
dreamPatch = {
|
|
2487
|
+
...(cfgBefore.dream ?? {}),
|
|
2488
|
+
provider: accepted.dream.provider,
|
|
2489
|
+
model: accepted.dream.model,
|
|
2490
|
+
};
|
|
2491
|
+
if (!dreamEnabledBefore &&
|
|
2492
|
+
(await askYesNo(rl, "Enable dream mode with this routing?", true))) {
|
|
2493
|
+
dreamPatch.enabled = true;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
const updatePayload = {};
|
|
2497
|
+
if (taskModelsPatch)
|
|
2498
|
+
updatePayload.taskModels = taskModelsPatch;
|
|
2499
|
+
if (dreamPatch)
|
|
2500
|
+
updatePayload.dream = dreamPatch;
|
|
2501
|
+
if (!taskModelsPatch && !dreamPatch) {
|
|
2502
|
+
printStatus("warn", "no routing changes to save");
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
await updateConfig(storePath, updatePayload);
|
|
2506
|
+
printStatus("ok", `saved task routing · ${storePath}/gnosys.json`);
|
|
2073
2507
|
}
|
|
2074
2508
|
/**
|
|
2075
2509
|
* Walks the user through configuring dream mode. Handles:
|
|
@@ -2143,8 +2577,12 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2143
2577
|
dream: { ...(existingDream ?? {}), enabled: false },
|
|
2144
2578
|
});
|
|
2145
2579
|
clearDreamMachineEverywhere();
|
|
2580
|
+
const { uninstallDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
2581
|
+
const launchdPath = uninstallDreamLaunchAgent();
|
|
2146
2582
|
console.log();
|
|
2147
2583
|
printStatus("ok", "dream mode disabled · designation cleared");
|
|
2584
|
+
if (launchdPath)
|
|
2585
|
+
printStatus("ok", "launchd agent removed", launchdPath);
|
|
2148
2586
|
localDb.close();
|
|
2149
2587
|
remoteDb?.close();
|
|
2150
2588
|
return;
|
|
@@ -2201,26 +2639,27 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2201
2639
|
}
|
|
2202
2640
|
// Validate — animated Spinner with the model latency reported.
|
|
2203
2641
|
if (dreamProvider !== "skip") {
|
|
2204
|
-
const apiKey = await getApiKeyForProvider(dreamProvider);
|
|
2642
|
+
const apiKey = await getApiKeyForProvider(dreamProvider, { task: "dream" });
|
|
2205
2643
|
const isLocalProvider = dreamProvider === "ollama" || dreamProvider === "lmstudio";
|
|
2206
2644
|
if (apiKey || isLocalProvider) {
|
|
2207
2645
|
console.log();
|
|
2208
|
-
const validateSpin = Spinner(`validating ${dreamProvider} / ${dreamModel}…`);
|
|
2209
2646
|
try {
|
|
2210
2647
|
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
|
-
|
|
2648
|
+
const { proceed } = await validateModelWithKeyRotation({
|
|
2649
|
+
rl,
|
|
2650
|
+
provider: dreamProvider,
|
|
2651
|
+
model: dreamModel,
|
|
2652
|
+
apiKey,
|
|
2653
|
+
customBaseUrl,
|
|
2654
|
+
isLocalProvider,
|
|
2655
|
+
saveAnywayPrompt: "save config anyway?",
|
|
2656
|
+
saveAnywayDefault: true,
|
|
2657
|
+
});
|
|
2658
|
+
if (!proceed) {
|
|
2659
|
+
printStatus("warn", "setup cancelled · no changes written");
|
|
2660
|
+
localDb.close();
|
|
2661
|
+
remoteDb?.close();
|
|
2662
|
+
return;
|
|
2224
2663
|
}
|
|
2225
2664
|
}
|
|
2226
2665
|
catch (err) {
|
|
@@ -2228,7 +2667,7 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2228
2667
|
}
|
|
2229
2668
|
}
|
|
2230
2669
|
else {
|
|
2231
|
-
printStatus("warn", `no API key for ${dreamProvider}`, "set one via `gnosys setup
|
|
2670
|
+
printStatus("warn", `no API key for ${dreamProvider}`, "set one via `gnosys setup providers`");
|
|
2232
2671
|
}
|
|
2233
2672
|
}
|
|
2234
2673
|
// ─── 7.2 Thresholds + sub-tasks ───────────────────────────────────
|
|
@@ -2241,6 +2680,12 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2241
2680
|
const dIdle = existingDream?.idleMinutes ?? 10;
|
|
2242
2681
|
const dRuntime = existingDream?.maxRuntimeMinutes ?? 30;
|
|
2243
2682
|
const dMinMem = existingDream?.minMemories ?? 10;
|
|
2683
|
+
const dScheduleStart = existingDream?.schedule?.startHour ?? 2;
|
|
2684
|
+
const dScheduleEnd = existingDream?.schedule?.endHour ?? 5;
|
|
2685
|
+
const dSystemIdle = existingDream?.systemIdleMinutes ?? 30;
|
|
2686
|
+
const dMinNewMemories = existingDream?.minNewMemoriesToDream ?? 10;
|
|
2687
|
+
const dMinHours = existingDream?.minHoursBetweenRuns ?? 20;
|
|
2688
|
+
const dMaxCalls = existingDream?.maxLLMCallsPerRun ?? 12;
|
|
2244
2689
|
const dSelfCritique = existingDream?.selfCritique ?? true;
|
|
2245
2690
|
const dGenSummaries = existingDream?.generateSummaries ?? true;
|
|
2246
2691
|
const dDiscover = existingDream?.discoverRelationships ?? true;
|
|
@@ -2257,16 +2702,34 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2257
2702
|
let idleMinutes = dIdle;
|
|
2258
2703
|
let maxRuntimeMinutes = dRuntime;
|
|
2259
2704
|
let minMemories = dMinMem;
|
|
2705
|
+
let scheduleStartHour = dScheduleStart;
|
|
2706
|
+
let scheduleEndHour = dScheduleEnd;
|
|
2707
|
+
let systemIdleMinutes = dSystemIdle;
|
|
2708
|
+
let minNewMemoriesToDream = dMinNewMemories;
|
|
2709
|
+
let minHoursBetweenRuns = dMinHours;
|
|
2710
|
+
let maxLLMCallsPerRun = dMaxCalls;
|
|
2260
2711
|
let selfCritique = dSelfCritique;
|
|
2261
2712
|
let generateSummaries = dGenSummaries;
|
|
2262
2713
|
let discoverRelationships = dDiscover;
|
|
2263
2714
|
if (editChoice === "e") {
|
|
2264
2715
|
const idleAns = await askInput(rl, "idle minutes before triggering", { default: String(dIdle) });
|
|
2265
|
-
idleMinutes = Math.max(1, parseInt(idleAns) || dIdle);
|
|
2716
|
+
idleMinutes = Math.max(1, parseInt(idleAns, 10) || dIdle);
|
|
2266
2717
|
const runtimeAns = await askInput(rl, "max runtime minutes", { default: String(dRuntime) });
|
|
2267
|
-
maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns) || dRuntime);
|
|
2718
|
+
maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns, 10) || dRuntime);
|
|
2268
2719
|
const minMemAns = await askInput(rl, "minimum memories before activating", { default: String(dMinMem) });
|
|
2269
|
-
minMemories = Math.max(1, parseInt(minMemAns) || dMinMem);
|
|
2720
|
+
minMemories = Math.max(1, parseInt(minMemAns, 10) || dMinMem);
|
|
2721
|
+
const scheduleStartAns = await askInput(rl, "night window start hour (0-23)", { default: String(dScheduleStart) });
|
|
2722
|
+
scheduleStartHour = Math.min(23, Math.max(0, parseInt(scheduleStartAns, 10) || dScheduleStart));
|
|
2723
|
+
const scheduleEndAns = await askInput(rl, "night window end hour (0-23)", { default: String(dScheduleEnd) });
|
|
2724
|
+
scheduleEndHour = Math.min(23, Math.max(0, parseInt(scheduleEndAns, 10) || dScheduleEnd));
|
|
2725
|
+
const systemIdleAns = await askInput(rl, "real machine idle minutes required", { default: String(dSystemIdle) });
|
|
2726
|
+
systemIdleMinutes = Math.max(1, parseInt(systemIdleAns, 10) || dSystemIdle);
|
|
2727
|
+
const minNewAns = await askInput(rl, "minimum changed memories before dreaming", { default: String(dMinNewMemories) });
|
|
2728
|
+
minNewMemoriesToDream = Math.max(0, parseInt(minNewAns, 10) || dMinNewMemories);
|
|
2729
|
+
const minHoursAns = await askInput(rl, "minimum hours between successful dreams", { default: String(dMinHours) });
|
|
2730
|
+
minHoursBetweenRuns = Math.max(0, parseInt(minHoursAns, 10) || dMinHours);
|
|
2731
|
+
const maxCallsAns = await askInput(rl, "maximum LLM calls per dream run", { default: String(dMaxCalls) });
|
|
2732
|
+
maxLLMCallsPerRun = Math.max(0, parseInt(maxCallsAns, 10) || dMaxCalls);
|
|
2270
2733
|
selfCritique = await askYesNo(rl, "self-critique (rule + LLM-based review flagging)", dSelfCritique);
|
|
2271
2734
|
generateSummaries = await askYesNo(rl, "generate summaries (LLM)", dGenSummaries);
|
|
2272
2735
|
discoverRelationships = await askYesNo(rl, "discover relationships (LLM)", dDiscover);
|
|
@@ -2281,6 +2744,11 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2281
2744
|
minMemories,
|
|
2282
2745
|
provider: dreamProvider,
|
|
2283
2746
|
model: dreamModel || undefined,
|
|
2747
|
+
schedule: { startHour: scheduleStartHour, endHour: scheduleEndHour },
|
|
2748
|
+
systemIdleMinutes,
|
|
2749
|
+
minNewMemoriesToDream,
|
|
2750
|
+
minHoursBetweenRuns,
|
|
2751
|
+
maxLLMCallsPerRun,
|
|
2284
2752
|
selfCritique,
|
|
2285
2753
|
generateSummaries,
|
|
2286
2754
|
discoverRelationships,
|
|
@@ -2289,6 +2757,8 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2289
2757
|
// Reset consecutive failure counter on a fresh setup so Layer 4
|
|
2290
2758
|
// doesn't fire immediately based on stale history.
|
|
2291
2759
|
localDb.resetDreamConsecutiveFailures();
|
|
2760
|
+
const { installDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
2761
|
+
const launchdPath = installDreamLaunchAgent();
|
|
2292
2762
|
localDb.close();
|
|
2293
2763
|
remoteDb?.close();
|
|
2294
2764
|
// Final Diff block per the design — provider/machine + the two
|
|
@@ -2316,7 +2786,9 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2316
2786
|
discoverRelationships,
|
|
2317
2787
|
}));
|
|
2318
2788
|
const dreamerName = designate ? localMachine : (designatedMachine ?? "the designated machine");
|
|
2319
|
-
printStatus("progress", `
|
|
2789
|
+
printStatus("progress", `scheduled dream checks run nightly (${scheduleStartHour}:00-${scheduleEndHour}:00) on ${dreamerName}`);
|
|
2790
|
+
if (launchdPath)
|
|
2791
|
+
printStatus("ok", "launchd agent installed", launchdPath);
|
|
2320
2792
|
printStatus("progress", "check status anytime with `gnosys status --system`");
|
|
2321
2793
|
}
|
|
2322
2794
|
finally {
|
|
@@ -2379,29 +2851,17 @@ async function openRemoteDbIfConfigured(localDb) {
|
|
|
2379
2851
|
* Best-effort lookup of the API key for a provider. Used by the dream setup
|
|
2380
2852
|
* wizard to power the validation step. Mirrors the resolveApiKey precedence.
|
|
2381
2853
|
*/
|
|
2382
|
-
export async function getApiKeyForProvider(provider) {
|
|
2383
|
-
if (provider === "ollama" || provider === "lmstudio" || provider === "skip")
|
|
2854
|
+
export async function getApiKeyForProvider(provider, opts) {
|
|
2855
|
+
if (provider === "ollama" || provider === "lmstudio" || provider === "skip") {
|
|
2384
2856
|
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
|
|
2857
|
+
}
|
|
2858
|
+
if (opts?.directory) {
|
|
2859
|
+
const cfg = await loadExistingConfig(opts.directory);
|
|
2860
|
+
if (cfg) {
|
|
2861
|
+
const fromCfg = getApiKeyForProviderFromConfig(cfg, provider);
|
|
2862
|
+
if (fromCfg)
|
|
2863
|
+
return fromCfg;
|
|
2404
2864
|
}
|
|
2405
2865
|
}
|
|
2406
|
-
return "";
|
|
2866
|
+
return readFirstInChain(provider) ?? "";
|
|
2407
2867
|
}
|