gnosys 5.11.3 → 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 +332 -5151
- 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/ideMcpInstall.d.ts +30 -0
- package/dist/lib/ideMcpInstall.js +102 -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/ides.d.ts +7 -0
- package/dist/lib/setup/sections/ides.js +24 -2
- 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 +813 -303
- 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,11 +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";
|
|
25
|
+
import { gnosysStdioMcpEntry, installStdioMcpJson, normalizeIdeKey, resolveGnosysMcpCommand, cursorMcpPaths, runCli, removeTomlSection, } from "./ideMcpInstall.js";
|
|
26
|
+
import { runKeysSetup } from "./setupKeys.js";
|
|
22
27
|
// ─── ANSI Colors ────────────────────────────────────────────────────────────
|
|
23
28
|
const BOLD = "\x1b[1m";
|
|
24
29
|
const DIM = "\x1b[2m";
|
|
@@ -75,6 +80,7 @@ export const PROVIDER_TIERS = {
|
|
|
75
80
|
lmstudio: [
|
|
76
81
|
{ name: "Default", model: "default", input: 0, output: 0, recommended: true },
|
|
77
82
|
],
|
|
83
|
+
openrouter: OPENROUTER_STATIC_TIERS,
|
|
78
84
|
custom: [],
|
|
79
85
|
};
|
|
80
86
|
// ─── Dynamic Model Fetching (OpenRouter) ─────────────────────────────────────
|
|
@@ -94,7 +100,7 @@ const OPENROUTER_PREFIX_MAP = {
|
|
|
94
100
|
* Fetch models from OpenRouter, cache for 24 hours, fall back to hardcoded.
|
|
95
101
|
* Returns updated PROVIDER_TIERS for cloud providers only.
|
|
96
102
|
*/
|
|
97
|
-
async function fetchDynamicModels() {
|
|
103
|
+
export async function fetchDynamicModels() {
|
|
98
104
|
// Check cache first
|
|
99
105
|
try {
|
|
100
106
|
const stat = await fs.stat(CACHE_FILE);
|
|
@@ -249,6 +255,7 @@ async function fetchDynamicModels() {
|
|
|
249
255
|
}
|
|
250
256
|
result[ourProvider] = tiers;
|
|
251
257
|
}
|
|
258
|
+
result.openrouter = buildOpenRouterTiers(data.data);
|
|
252
259
|
// Cache the result
|
|
253
260
|
try {
|
|
254
261
|
await fs.mkdir(path.dirname(CACHE_FILE), { recursive: true });
|
|
@@ -283,6 +290,7 @@ const PROVIDER_DISPLAY = {
|
|
|
283
290
|
xai: "xAI (Grok)",
|
|
284
291
|
mistral: "Mistral",
|
|
285
292
|
lmstudio: "LM Studio (local, free)",
|
|
293
|
+
openrouter: "OpenRouter (free + paid models)",
|
|
286
294
|
custom: "Custom (any OpenAI-compatible API)",
|
|
287
295
|
};
|
|
288
296
|
const PROVIDER_ENV_VAR = {
|
|
@@ -291,6 +299,7 @@ const PROVIDER_ENV_VAR = {
|
|
|
291
299
|
groq: "GNOSYS_GROQ_KEY",
|
|
292
300
|
xai: "GNOSYS_XAI_KEY",
|
|
293
301
|
mistral: "GNOSYS_MISTRAL_KEY",
|
|
302
|
+
openrouter: "GNOSYS_OPENROUTER_KEY",
|
|
294
303
|
custom: "GNOSYS_CUSTOM_KEY",
|
|
295
304
|
};
|
|
296
305
|
// Ordered list for the menu
|
|
@@ -302,16 +311,40 @@ const PROVIDER_ORDER = [
|
|
|
302
311
|
"xai",
|
|
303
312
|
"mistral",
|
|
304
313
|
"lmstudio",
|
|
314
|
+
"openrouter",
|
|
305
315
|
"custom",
|
|
306
316
|
];
|
|
307
317
|
// Task descriptions for display
|
|
308
|
-
const TASK_DESCRIPTIONS = {
|
|
318
|
+
export const TASK_DESCRIPTIONS = {
|
|
309
319
|
structuring: "adding memories, tagging",
|
|
310
320
|
synthesis: "Q&A answers",
|
|
321
|
+
chat: "interactive chat TUI",
|
|
311
322
|
vision: "images, PDFs",
|
|
312
323
|
transcription: "audio files",
|
|
313
324
|
dream: "overnight consolidation",
|
|
314
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
|
+
}
|
|
315
348
|
// ─── Exported Helpers ───────────────────────────────────────────────────────
|
|
316
349
|
/**
|
|
317
350
|
* Returns the cheapest capable model for structuring tasks.
|
|
@@ -333,8 +366,10 @@ export function getStructuringModel(provider, chosenModel) {
|
|
|
333
366
|
* Creates the directory and file if they don't exist.
|
|
334
367
|
* Replaces an existing key line if found, otherwise appends.
|
|
335
368
|
*/
|
|
336
|
-
export async function writeApiKey(provider, key) {
|
|
337
|
-
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];
|
|
338
373
|
if (!envVar)
|
|
339
374
|
return;
|
|
340
375
|
const configDir = path.join(os.homedir(), ".config", "gnosys");
|
|
@@ -417,6 +452,68 @@ function hasSecretTool() {
|
|
|
417
452
|
return false;
|
|
418
453
|
}
|
|
419
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
|
+
}
|
|
420
517
|
/**
|
|
421
518
|
* Detect where an existing API key is stored.
|
|
422
519
|
* Returns description like "macOS Keychain", "env var", ".env file", or empty string.
|
|
@@ -567,24 +664,6 @@ export async function detectIDEs(projectDir) {
|
|
|
567
664
|
}
|
|
568
665
|
return detected;
|
|
569
666
|
}
|
|
570
|
-
/** Remove a single `[section]` block from Grok TOML (hand-rolled; see upsertGrokMcpBlock). */
|
|
571
|
-
function removeGrokTomlSection(existing, sectionHeader) {
|
|
572
|
-
const lines = existing.split("\n");
|
|
573
|
-
const headerIdx = lines.findIndex((line) => line.trim() === sectionHeader);
|
|
574
|
-
if (headerIdx === -1)
|
|
575
|
-
return existing;
|
|
576
|
-
let endIdx = lines.length;
|
|
577
|
-
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
578
|
-
if (/^\s*\[/.test(lines[i])) {
|
|
579
|
-
endIdx = i;
|
|
580
|
-
break;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
const before = lines.slice(0, headerIdx).join("\n");
|
|
584
|
-
const after = lines.slice(endIdx).join("\n");
|
|
585
|
-
const merged = [before, after].filter((s) => s.length > 0).join("\n");
|
|
586
|
-
return merged.length > 0 ? `${merged}\n` : "";
|
|
587
|
-
}
|
|
588
667
|
/**
|
|
589
668
|
* Replace (or append) a `[mcp_servers.<name>]` block inside Grok Build's
|
|
590
669
|
* `~/.grok/config.toml`. Preserves every line outside that block (deci-046).
|
|
@@ -596,7 +675,7 @@ function removeGrokTomlSection(existing, sectionHeader) {
|
|
|
596
675
|
*/
|
|
597
676
|
export function upsertGrokMcpBlock(existing, name, entry) {
|
|
598
677
|
// Drop mistaken v5.9.4 `[mcp.<name>]` sections so we don't leave dead config.
|
|
599
|
-
let content =
|
|
678
|
+
let content = removeTomlSection(existing, `[mcp.${name}]`);
|
|
600
679
|
const sectionHeader = `[mcp_servers.${name}]`;
|
|
601
680
|
const lines = content.split("\n");
|
|
602
681
|
const headerIdx = lines.findIndex((line) => line.trim() === sectionHeader);
|
|
@@ -632,25 +711,6 @@ export function upsertGrokMcpBlock(existing, name, entry) {
|
|
|
632
711
|
const afterBlock = afterLines.join("\n");
|
|
633
712
|
return `${head}${sectionHeader}\n${blockBody}${gap}${afterBlock}`;
|
|
634
713
|
}
|
|
635
|
-
/**
|
|
636
|
-
* Absolute path to the `gnosys-mcp` stdio entry (dist/index.js).
|
|
637
|
-
* Prefer this over `gnosys serve` — v5.11.0 `gnosys serve` imported index.js but
|
|
638
|
-
* did not call startMcpServer(), so MCP hosts saw "connection closed" on init.
|
|
639
|
-
*/
|
|
640
|
-
function resolveGnosysMcpCommand() {
|
|
641
|
-
try {
|
|
642
|
-
const p = execSync("command -v gnosys-mcp", { encoding: "utf-8" }).trim();
|
|
643
|
-
if (p)
|
|
644
|
-
return p;
|
|
645
|
-
}
|
|
646
|
-
catch {
|
|
647
|
-
// Fall back to bare name on PATH.
|
|
648
|
-
}
|
|
649
|
-
return "gnosys-mcp";
|
|
650
|
-
}
|
|
651
|
-
function gnosysMcpServerEntry() {
|
|
652
|
-
return { command: resolveGnosysMcpCommand(), args: [] };
|
|
653
|
-
}
|
|
654
714
|
function renderGrokMcpBlock(entry) {
|
|
655
715
|
const argsStr = `[${entry.args.map((a) => JSON.stringify(a)).join(", ")}]`;
|
|
656
716
|
const lines = [
|
|
@@ -666,48 +726,45 @@ function renderGrokMcpBlock(entry) {
|
|
|
666
726
|
* Set up Gnosys MCP integration for a specific IDE.
|
|
667
727
|
*/
|
|
668
728
|
export async function setupIDE(ide, projectDir) {
|
|
729
|
+
const canonical = normalizeIdeKey(ide);
|
|
730
|
+
if (!canonical) {
|
|
731
|
+
return { success: false, message: `Unknown IDE: ${ide}` };
|
|
732
|
+
}
|
|
669
733
|
try {
|
|
670
|
-
switch (
|
|
734
|
+
switch (canonical) {
|
|
671
735
|
case "claude": {
|
|
672
736
|
const mcpCmd = resolveGnosysMcpCommand();
|
|
737
|
+
let codeMsg = `Claude Code MCP registered (${mcpCmd})`;
|
|
673
738
|
try {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
catch {
|
|
678
|
-
// Not registered yet — fine.
|
|
679
|
-
}
|
|
680
|
-
execSync(`claude mcp add -s user gnosys -- ${mcpCmd}`, {
|
|
681
|
-
stdio: "pipe",
|
|
682
|
-
});
|
|
739
|
+
runCli("claude", ["mcp", "remove", "gnosys"], { allowFailure: true });
|
|
740
|
+
runCli("claude", ["mcp", "add", "-s", "user", "gnosys", "--", mcpCmd]);
|
|
683
741
|
}
|
|
684
742
|
catch (e) {
|
|
685
743
|
const msg = e instanceof Error ? e.message : String(e);
|
|
686
744
|
if (msg.includes("already exists")) {
|
|
687
|
-
|
|
745
|
+
codeMsg = "Claude Code MCP server already configured";
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
codeMsg = `Claude Code MCP skipped (is \`claude\` on PATH?): ${msg}`;
|
|
688
749
|
}
|
|
689
|
-
throw e;
|
|
690
750
|
}
|
|
691
|
-
|
|
751
|
+
const desktop = await setupIDE("claude-desktop", projectDir);
|
|
752
|
+
return {
|
|
753
|
+
success: desktop.success,
|
|
754
|
+
message: desktop.success
|
|
755
|
+
? `${codeMsg}; ${desktop.message}`
|
|
756
|
+
: `${codeMsg}; Claude Desktop: ${desktop.message}`,
|
|
757
|
+
};
|
|
692
758
|
}
|
|
693
759
|
case "cursor": {
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
await
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
catch {
|
|
703
|
-
// File doesn't exist or is invalid — start fresh
|
|
704
|
-
}
|
|
705
|
-
// Merge gnosys entry
|
|
706
|
-
const servers = (config.mcpServers ?? {});
|
|
707
|
-
servers.gnosys = gnosysMcpServerEntry();
|
|
708
|
-
config.mcpServers = servers;
|
|
709
|
-
await fs.writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
710
|
-
return { success: true, message: "Cursor MCP config updated (.cursor/mcp.json)" };
|
|
760
|
+
const paths = cursorMcpPaths(projectDir);
|
|
761
|
+
await fs.mkdir(path.dirname(paths.project), { recursive: true });
|
|
762
|
+
await installStdioMcpJson(paths.project);
|
|
763
|
+
await installStdioMcpJson(paths.user);
|
|
764
|
+
return {
|
|
765
|
+
success: true,
|
|
766
|
+
message: "Cursor MCP updated (project .cursor/mcp.json and ~/.cursor/mcp.json)",
|
|
767
|
+
};
|
|
711
768
|
}
|
|
712
769
|
case "codex": {
|
|
713
770
|
// v5.8.5: register via `codex mcp add` (the real Codex CLI registration
|
|
@@ -719,19 +776,13 @@ export async function setupIDE(ide, projectDir) {
|
|
|
719
776
|
//
|
|
720
777
|
// We also migrate away from those legacy blocks in
|
|
721
778
|
// `~/.codex/config.toml` so users on stale configs get cleaned up.
|
|
722
|
-
|
|
723
|
-
// 1. Migrate (strip) legacy hand-written blocks in ~/.codex/config.toml.
|
|
779
|
+
// 1. Strip legacy hand-written sections in ~/.codex/config.toml.
|
|
724
780
|
const userCodexConfig = path.join(os.homedir(), ".codex", "config.toml");
|
|
725
781
|
try {
|
|
726
782
|
let existing = await fs.readFile(userCodexConfig, "utf-8");
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
// v5.8.4 shape: [mcp.gnosys] type/command
|
|
731
|
-
existing = existing.replace(/\n?\[mcp\.gnosys\][^[]*?type\s*=\s*"local"[^[]*?command\s*=\s*\[[^\]]*\]\s*\n?/, "\n");
|
|
732
|
-
if (existing !== before) {
|
|
733
|
-
existing = existing.replace(/\n{3,}/g, "\n\n");
|
|
734
|
-
await fs.writeFile(userCodexConfig, existing, "utf-8");
|
|
783
|
+
const cleaned = removeTomlSection(removeTomlSection(existing, "[mcp.gnosys]"), "[gnosys]");
|
|
784
|
+
if (cleaned !== existing) {
|
|
785
|
+
await fs.writeFile(userCodexConfig, cleaned, "utf-8");
|
|
735
786
|
}
|
|
736
787
|
}
|
|
737
788
|
catch {
|
|
@@ -744,25 +795,16 @@ export async function setupIDE(ide, projectDir) {
|
|
|
744
795
|
// remove and re-add.
|
|
745
796
|
let alreadyCorrect = false;
|
|
746
797
|
try {
|
|
747
|
-
const existing =
|
|
748
|
-
encoding: "utf-8",
|
|
749
|
-
});
|
|
798
|
+
const existing = runCli("codex", ["mcp", "get", "gnosys"], { allowFailure: true });
|
|
750
799
|
if (existing && existing.includes(gnosysCmd) && !existing.includes(" serve")) {
|
|
751
800
|
alreadyCorrect = true;
|
|
752
801
|
}
|
|
753
802
|
else if (existing) {
|
|
754
|
-
|
|
755
|
-
try {
|
|
756
|
-
execSync("codex mcp remove gnosys", { stdio: "pipe" });
|
|
757
|
-
}
|
|
758
|
-
catch {
|
|
759
|
-
// Non-fatal — `mcp add` below will overwrite or fail loudly.
|
|
760
|
-
}
|
|
803
|
+
runCli("codex", ["mcp", "remove", "gnosys"], { allowFailure: true });
|
|
761
804
|
}
|
|
762
805
|
}
|
|
763
806
|
catch {
|
|
764
|
-
// `codex mcp get`
|
|
765
|
-
// that's the common case on first install; proceed to add.
|
|
807
|
+
// `codex mcp get` failed — proceed to add.
|
|
766
808
|
}
|
|
767
809
|
if (alreadyCorrect) {
|
|
768
810
|
return {
|
|
@@ -772,7 +814,7 @@ export async function setupIDE(ide, projectDir) {
|
|
|
772
814
|
}
|
|
773
815
|
// 4. Register via the canonical Codex CLI command.
|
|
774
816
|
try {
|
|
775
|
-
|
|
817
|
+
runCli("codex", ["mcp", "add", "gnosys", "--", gnosysCmd]);
|
|
776
818
|
}
|
|
777
819
|
catch (err) {
|
|
778
820
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -789,43 +831,19 @@ export async function setupIDE(ide, projectDir) {
|
|
|
789
831
|
};
|
|
790
832
|
}
|
|
791
833
|
case "gemini-cli": {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
await fs.mkdir(geminiDir, { recursive: true });
|
|
796
|
-
let config = {};
|
|
797
|
-
try {
|
|
798
|
-
const existing = await fs.readFile(settingsPath, "utf-8");
|
|
799
|
-
config = JSON.parse(existing);
|
|
800
|
-
}
|
|
801
|
-
catch {
|
|
802
|
-
// File doesn't exist or is invalid — start fresh
|
|
803
|
-
}
|
|
804
|
-
const servers = (config.mcpServers ?? {});
|
|
805
|
-
servers.gnosys = gnosysMcpServerEntry();
|
|
806
|
-
config.mcpServers = servers;
|
|
807
|
-
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
834
|
+
const settingsPath = path.join(os.homedir(), ".gemini", "settings.json");
|
|
835
|
+
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
836
|
+
await installStdioMcpJson(settingsPath);
|
|
808
837
|
return { success: true, message: "Gemini CLI MCP config updated (~/.gemini/settings.json)" };
|
|
809
838
|
}
|
|
810
839
|
case "antigravity": {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const existing = await fs.readFile(configPath, "utf-8");
|
|
819
|
-
config = JSON.parse(existing);
|
|
820
|
-
}
|
|
821
|
-
catch {
|
|
822
|
-
// File doesn't exist or is invalid — start fresh
|
|
823
|
-
}
|
|
824
|
-
const servers = (config.mcpServers ?? {});
|
|
825
|
-
servers.gnosys = gnosysMcpServerEntry();
|
|
826
|
-
config.mcpServers = servers;
|
|
827
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
828
|
-
return { success: true, message: "Antigravity MCP config updated (~/.gemini/antigravity/mcp_config.json)" };
|
|
840
|
+
const configPath = path.join(os.homedir(), ".gemini", "antigravity", "mcp_config.json");
|
|
841
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
842
|
+
await installStdioMcpJson(configPath);
|
|
843
|
+
return {
|
|
844
|
+
success: true,
|
|
845
|
+
message: "Antigravity MCP config updated (~/.gemini/antigravity/mcp_config.json)",
|
|
846
|
+
};
|
|
829
847
|
}
|
|
830
848
|
case "grok-build": {
|
|
831
849
|
// v5.9.4 Bug 12 — Grok Build reads MCP from `[mcp_servers.<name>]`
|
|
@@ -842,32 +860,16 @@ export async function setupIDE(ide, projectDir) {
|
|
|
842
860
|
// File doesn't exist yet — start fresh
|
|
843
861
|
}
|
|
844
862
|
const updated = upsertGrokMcpBlock(existing, "gnosys", {
|
|
845
|
-
...
|
|
863
|
+
...gnosysStdioMcpEntry(),
|
|
846
864
|
startup_timeout_sec: 90,
|
|
847
865
|
});
|
|
848
866
|
await fs.writeFile(configPath, updated, "utf-8");
|
|
849
867
|
return { success: true, message: "Grok Build MCP config updated (~/.grok/config.toml)" };
|
|
850
868
|
}
|
|
851
869
|
case "claude-desktop": {
|
|
852
|
-
// Claude Desktop reads MCP servers from claude_desktop_config.json
|
|
853
|
-
// in a platform-specific app data directory. Distinct from Claude
|
|
854
|
-
// Code CLI which uses `claude mcp add`.
|
|
855
870
|
const configPath = getClaudeDesktopConfigPath();
|
|
856
|
-
|
|
857
|
-
await
|
|
858
|
-
let config = {};
|
|
859
|
-
try {
|
|
860
|
-
const existing = await fs.readFile(configPath, "utf-8");
|
|
861
|
-
config = JSON.parse(existing);
|
|
862
|
-
}
|
|
863
|
-
catch {
|
|
864
|
-
// File doesn't exist or is invalid — start fresh
|
|
865
|
-
}
|
|
866
|
-
const servers = (config.mcpServers ?? {});
|
|
867
|
-
servers.gnosys = gnosysMcpServerEntry();
|
|
868
|
-
config.mcpServers = servers;
|
|
869
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
870
|
-
// Display path with ~ prefix when inside HOME for clarity
|
|
871
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
872
|
+
await installStdioMcpJson(configPath);
|
|
871
873
|
const home = os.homedir();
|
|
872
874
|
const displayPath = configPath.startsWith(home)
|
|
873
875
|
? configPath.replace(home, "~")
|
|
@@ -877,9 +879,8 @@ export async function setupIDE(ide, projectDir) {
|
|
|
877
879
|
message: `Claude Desktop MCP config updated (${displayPath}). Restart Claude Desktop for the change to take effect.`,
|
|
878
880
|
};
|
|
879
881
|
}
|
|
880
|
-
default:
|
|
881
|
-
return { success: false, message: `Unknown IDE: ${ide}` };
|
|
882
882
|
}
|
|
883
|
+
return { success: false, message: `Unhandled IDE: ${canonical}` };
|
|
883
884
|
}
|
|
884
885
|
catch (err) {
|
|
885
886
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -912,16 +913,20 @@ async function askChoice(rl, question, options) {
|
|
|
912
913
|
/**
|
|
913
914
|
* Read a single line of input with an optional default value.
|
|
914
915
|
*/
|
|
915
|
-
async function askInput(rl, prompt, opts) {
|
|
916
|
+
export async function askInput(rl, prompt, opts) {
|
|
916
917
|
const suffix = opts?.default ? ` ${DIM}(${opts.default})${RESET}` : "";
|
|
917
918
|
const answer = await safeQuestion(rl, `${prompt}${suffix}: `);
|
|
918
919
|
const trimmed = answer.trim();
|
|
919
920
|
return trimmed || opts?.default || "";
|
|
920
921
|
}
|
|
922
|
+
export function printInfo(text, meta) {
|
|
923
|
+
const suffix = meta ? ` ${DIM}${meta}${RESET}` : "";
|
|
924
|
+
console.log(` ${text}${suffix}`);
|
|
925
|
+
}
|
|
921
926
|
/**
|
|
922
927
|
* Y/n prompt. Returns true for yes.
|
|
923
928
|
*/
|
|
924
|
-
async function askYesNo(rl, question, defaultYes = true) {
|
|
929
|
+
export async function askYesNo(rl, question, defaultYes = true) {
|
|
925
930
|
const hint = defaultYes ? "Y/n" : "y/N";
|
|
926
931
|
const answer = await safeQuestion(rl, `${question} [${hint}] `);
|
|
927
932
|
const trimmed = answer.trim().toLowerCase();
|
|
@@ -929,6 +934,78 @@ async function askYesNo(rl, question, defaultYes = true) {
|
|
|
929
934
|
return defaultYes;
|
|
930
935
|
return trimmed === "y" || trimmed === "yes";
|
|
931
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
|
+
}
|
|
932
1009
|
/**
|
|
933
1010
|
* Format a price for display: "$0.80" or "free".
|
|
934
1011
|
*/
|
|
@@ -1021,7 +1098,7 @@ async function loadExistingConfig(projectDir) {
|
|
|
1021
1098
|
* Returns the provider name or "skip".
|
|
1022
1099
|
* If currentProvider is given, shows it as the current value.
|
|
1023
1100
|
*/
|
|
1024
|
-
async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
1101
|
+
export async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
1025
1102
|
const currentHint = currentProvider ? ` ${DIM}(current: ${currentProvider})${RESET}` : "";
|
|
1026
1103
|
const providerOptions = PROVIDER_ORDER.map((key) => {
|
|
1027
1104
|
const tiers = dynamicModels[key] ?? PROVIDER_TIERS[key];
|
|
@@ -1042,7 +1119,7 @@ async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
|
1042
1119
|
* Returns the model string. Includes a "Custom (enter model name)"
|
|
1043
1120
|
* option so users can type any model ID not in the curated list.
|
|
1044
1121
|
*/
|
|
1045
|
-
async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
1122
|
+
export async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
1046
1123
|
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
1047
1124
|
if (!tiers || tiers.length === 0) {
|
|
1048
1125
|
// No tiers available — fall back to direct entry
|
|
@@ -1066,8 +1143,10 @@ async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
|
1066
1143
|
}
|
|
1067
1144
|
return tiers[tierIndex].model;
|
|
1068
1145
|
}
|
|
1069
|
-
// ─── Main Setup Wizard ──────────────────────────────────────────────────────
|
|
1070
1146
|
export async function runSetup(opts) {
|
|
1147
|
+
if (opts.section === "keys") {
|
|
1148
|
+
return runKeysSetup();
|
|
1149
|
+
}
|
|
1071
1150
|
const version = getVersion();
|
|
1072
1151
|
const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
|
|
1073
1152
|
// ─── Non-interactive mode ─────────────────────────────────────────────
|
|
@@ -1294,6 +1373,7 @@ export async function runSetup(opts) {
|
|
|
1294
1373
|
groq: "GROQ_API_KEY",
|
|
1295
1374
|
xai: "XAI_API_KEY",
|
|
1296
1375
|
mistral: "MISTRAL_API_KEY",
|
|
1376
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
1297
1377
|
};
|
|
1298
1378
|
const legacyEnvVar = legacyEnvVars[provider] ?? "";
|
|
1299
1379
|
if (needsKey) {
|
|
@@ -1468,26 +1548,27 @@ export async function runSetup(opts) {
|
|
|
1468
1548
|
console.log();
|
|
1469
1549
|
console.log(`${DIM}Testing ${provider}/${model}...${RESET}`);
|
|
1470
1550
|
try {
|
|
1471
|
-
const { validateModel } = await import("./modelValidation.js");
|
|
1472
1551
|
const customBaseUrl = provider === "custom"
|
|
1473
1552
|
? process.env.GNOSYS_LLM_BASE_URL
|
|
1474
1553
|
: undefined;
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
+
};
|
|
1491
1572
|
}
|
|
1492
1573
|
}
|
|
1493
1574
|
catch (err) {
|
|
@@ -1766,6 +1847,18 @@ export async function runSetup(opts) {
|
|
|
1766
1847
|
await updateConfig(storePath, configUpdates);
|
|
1767
1848
|
console.log();
|
|
1768
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
|
+
}
|
|
1769
1862
|
}
|
|
1770
1863
|
catch (err) {
|
|
1771
1864
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1935,6 +2028,166 @@ export async function runProviderOnlySetup(opts = {}) {
|
|
|
1935
2028
|
rl.close();
|
|
1936
2029
|
}
|
|
1937
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
|
+
}
|
|
1938
2191
|
/**
|
|
1939
2192
|
* Models-only configuration — prompts for provider, model, and key (or accepts
|
|
1940
2193
|
* them via options for non-interactive use). Validates the model against the
|
|
@@ -1945,7 +2198,6 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1945
2198
|
const ownsRl = !opts.rl;
|
|
1946
2199
|
const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
|
|
1947
2200
|
try {
|
|
1948
|
-
// v5.9.3 Screen 3 — Header + Title at the top.
|
|
1949
2201
|
const { Header } = await import("./setup/ui/header.js");
|
|
1950
2202
|
const { Title } = await import("./setup/ui/title.js");
|
|
1951
2203
|
const { Spinner } = await import("./setup/ui/spinner.js");
|
|
@@ -1954,16 +2206,13 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1954
2206
|
console.log();
|
|
1955
2207
|
console.log(Header(["gnosys", "setup", "models"]));
|
|
1956
2208
|
console.log();
|
|
1957
|
-
console.log(Title("Model configuration", "pick a provider
|
|
2209
|
+
console.log(Title("Model configuration", "pick a provider — then default or per-task routing"));
|
|
1958
2210
|
console.log();
|
|
1959
2211
|
const existingConfig = await loadExistingConfig(projectDir);
|
|
1960
2212
|
const currentProvider = existingConfig?.llm.defaultProvider;
|
|
1961
2213
|
const currentModel = existingConfig
|
|
1962
2214
|
? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
|
|
1963
2215
|
: undefined;
|
|
1964
|
-
// Step 1: provider (or use --provider flag). v5.9.3: animate the
|
|
1965
|
-
// OpenRouter pricing fetch under a Spinner so the user gets feedback
|
|
1966
|
-
// on what would otherwise feel like a hang.
|
|
1967
2216
|
const pricingSpin = Spinner("fetching latest pricing from openrouter…");
|
|
1968
2217
|
const fetchStart = Date.now();
|
|
1969
2218
|
const dynamicModels = await fetchDynamicModels();
|
|
@@ -1973,8 +2222,6 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1973
2222
|
pricingSpin.ok(`pricing loaded · ${modelCount} models cached`, `${fetchMs} ms`);
|
|
1974
2223
|
}
|
|
1975
2224
|
else {
|
|
1976
|
-
// No-op fallback (cache miss + network fail) — keep the hardcoded
|
|
1977
|
-
// tiers but signal that we're running offline.
|
|
1978
2225
|
pricingSpin.fail("pricing fetch failed", "using bundled tiers");
|
|
1979
2226
|
}
|
|
1980
2227
|
console.log();
|
|
@@ -1990,103 +2237,339 @@ export async function runModelsSetup(opts = {}) {
|
|
|
1990
2237
|
else {
|
|
1991
2238
|
provider = await pickProvider(rl, dynamicModels, "Choose your LLM provider", currentProvider);
|
|
1992
2239
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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");
|
|
1998
2320
|
}
|
|
1999
2321
|
else {
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2322
|
+
const showCurrent = ctx.currentProvider === provider ? ctx.currentModel : undefined;
|
|
2323
|
+
model = await pickModel(rl, provider, dynamicModels, "Choose model", showCurrent);
|
|
2324
|
+
}
|
|
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.`);
|
|
2008
2338
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
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");
|
|
2011
2358
|
return;
|
|
2012
2359
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
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;
|
|
2032
2420
|
}
|
|
2033
|
-
if (
|
|
2034
|
-
|
|
2035
|
-
|
|
2421
|
+
if (parsed === "none") {
|
|
2422
|
+
selectedIndices = [];
|
|
2423
|
+
break;
|
|
2036
2424
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2425
|
+
if (parsed && parsed.length > 0) {
|
|
2426
|
+
selectedIndices = parsed;
|
|
2427
|
+
break;
|
|
2428
|
+
}
|
|
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;
|
|
2042
2456
|
console.log();
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
if (result.ok) {
|
|
2049
|
-
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;
|
|
2050
2462
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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);
|
|
2057
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;
|
|
2517
|
+
}
|
|
2518
|
+
if (retryChoice === 1) {
|
|
2519
|
+
continue;
|
|
2058
2520
|
}
|
|
2521
|
+
accepted[task] = cur;
|
|
2522
|
+
break;
|
|
2059
2523
|
}
|
|
2060
|
-
// Step 5: write config (v5.9.4 Bug 10 — unified store resolution).
|
|
2061
|
-
const storePath = ensureActiveStorePath(projectDir);
|
|
2062
|
-
const existingLlm = existingConfig?.llm;
|
|
2063
|
-
const existingProviderConfig = existingLlm
|
|
2064
|
-
? existingLlm[provider]
|
|
2065
|
-
: undefined;
|
|
2066
|
-
const providerConfigBase = (typeof existingProviderConfig === "object" && existingProviderConfig !== null)
|
|
2067
|
-
? existingProviderConfig
|
|
2068
|
-
: {};
|
|
2069
|
-
await updateConfig(storePath, {
|
|
2070
|
-
llm: {
|
|
2071
|
-
...(existingLlm ?? {}),
|
|
2072
|
-
defaultProvider: provider,
|
|
2073
|
-
[provider]: {
|
|
2074
|
-
...providerConfigBase,
|
|
2075
|
-
model,
|
|
2076
|
-
},
|
|
2077
|
-
},
|
|
2078
|
-
});
|
|
2079
|
-
// v5.9.3 Screen 3 — Diff() before the saved confirmation. Shows what
|
|
2080
|
-
// landed in gnosys.json.
|
|
2081
|
-
const { buildModelsDiffRows } = await import("./setup/modelsRender.js");
|
|
2082
|
-
console.log();
|
|
2083
|
-
printDiff(buildModelsDiffRows(currentProvider, currentModel, provider, model));
|
|
2084
|
-
printStatus("ok", `saved · ${storePath}/gnosys.json`);
|
|
2085
2524
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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}`);
|
|
2089
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;
|
|
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`);
|
|
2090
2573
|
}
|
|
2091
2574
|
/**
|
|
2092
2575
|
* Lightweight model-management command. Supports three operations:
|
|
@@ -2238,8 +2721,12 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2238
2721
|
dream: { ...(existingDream ?? {}), enabled: false },
|
|
2239
2722
|
});
|
|
2240
2723
|
clearDreamMachineEverywhere();
|
|
2724
|
+
const { uninstallDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
2725
|
+
const launchdPath = uninstallDreamLaunchAgent();
|
|
2241
2726
|
console.log();
|
|
2242
2727
|
printStatus("ok", "dream mode disabled · designation cleared");
|
|
2728
|
+
if (launchdPath)
|
|
2729
|
+
printStatus("ok", "launchd agent removed", launchdPath);
|
|
2243
2730
|
localDb.close();
|
|
2244
2731
|
remoteDb?.close();
|
|
2245
2732
|
return;
|
|
@@ -2296,26 +2783,27 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2296
2783
|
}
|
|
2297
2784
|
// Validate — animated Spinner with the model latency reported.
|
|
2298
2785
|
if (dreamProvider !== "skip") {
|
|
2299
|
-
const apiKey = await getApiKeyForProvider(dreamProvider);
|
|
2786
|
+
const apiKey = await getApiKeyForProvider(dreamProvider, { task: "dream" });
|
|
2300
2787
|
const isLocalProvider = dreamProvider === "ollama" || dreamProvider === "lmstudio";
|
|
2301
2788
|
if (apiKey || isLocalProvider) {
|
|
2302
2789
|
console.log();
|
|
2303
|
-
const validateSpin = Spinner(`validating ${dreamProvider} / ${dreamModel}…`);
|
|
2304
2790
|
try {
|
|
2305
2791
|
const customBaseUrl = dreamProvider === "custom" ? process.env.GNOSYS_LLM_BASE_URL : undefined;
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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;
|
|
2319
2807
|
}
|
|
2320
2808
|
}
|
|
2321
2809
|
catch (err) {
|
|
@@ -2323,7 +2811,7 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2323
2811
|
}
|
|
2324
2812
|
}
|
|
2325
2813
|
else {
|
|
2326
|
-
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`");
|
|
2327
2815
|
}
|
|
2328
2816
|
}
|
|
2329
2817
|
// ─── 7.2 Thresholds + sub-tasks ───────────────────────────────────
|
|
@@ -2336,6 +2824,12 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2336
2824
|
const dIdle = existingDream?.idleMinutes ?? 10;
|
|
2337
2825
|
const dRuntime = existingDream?.maxRuntimeMinutes ?? 30;
|
|
2338
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;
|
|
2339
2833
|
const dSelfCritique = existingDream?.selfCritique ?? true;
|
|
2340
2834
|
const dGenSummaries = existingDream?.generateSummaries ?? true;
|
|
2341
2835
|
const dDiscover = existingDream?.discoverRelationships ?? true;
|
|
@@ -2352,6 +2846,12 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2352
2846
|
let idleMinutes = dIdle;
|
|
2353
2847
|
let maxRuntimeMinutes = dRuntime;
|
|
2354
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;
|
|
2355
2855
|
let selfCritique = dSelfCritique;
|
|
2356
2856
|
let generateSummaries = dGenSummaries;
|
|
2357
2857
|
let discoverRelationships = dDiscover;
|
|
@@ -2362,6 +2862,18 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2362
2862
|
maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns) || dRuntime);
|
|
2363
2863
|
const minMemAns = await askInput(rl, "minimum memories before activating", { default: String(dMinMem) });
|
|
2364
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);
|
|
2365
2877
|
selfCritique = await askYesNo(rl, "self-critique (rule + LLM-based review flagging)", dSelfCritique);
|
|
2366
2878
|
generateSummaries = await askYesNo(rl, "generate summaries (LLM)", dGenSummaries);
|
|
2367
2879
|
discoverRelationships = await askYesNo(rl, "discover relationships (LLM)", dDiscover);
|
|
@@ -2376,6 +2888,11 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2376
2888
|
minMemories,
|
|
2377
2889
|
provider: dreamProvider,
|
|
2378
2890
|
model: dreamModel || undefined,
|
|
2891
|
+
schedule: { startHour: scheduleStartHour, endHour: scheduleEndHour },
|
|
2892
|
+
systemIdleMinutes,
|
|
2893
|
+
minNewMemoriesToDream,
|
|
2894
|
+
minHoursBetweenRuns,
|
|
2895
|
+
maxLLMCallsPerRun,
|
|
2379
2896
|
selfCritique,
|
|
2380
2897
|
generateSummaries,
|
|
2381
2898
|
discoverRelationships,
|
|
@@ -2384,6 +2901,8 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2384
2901
|
// Reset consecutive failure counter on a fresh setup so Layer 4
|
|
2385
2902
|
// doesn't fire immediately based on stale history.
|
|
2386
2903
|
localDb.resetDreamConsecutiveFailures();
|
|
2904
|
+
const { installDreamLaunchAgent } = await import("./dreamLaunchd.js");
|
|
2905
|
+
const launchdPath = installDreamLaunchAgent();
|
|
2387
2906
|
localDb.close();
|
|
2388
2907
|
remoteDb?.close();
|
|
2389
2908
|
// Final Diff block per the design — provider/machine + the two
|
|
@@ -2411,7 +2930,9 @@ export async function runDreamSetup(opts = {}) {
|
|
|
2411
2930
|
discoverRelationships,
|
|
2412
2931
|
}));
|
|
2413
2932
|
const dreamerName = designate ? localMachine : (designatedMachine ?? "the designated machine");
|
|
2414
|
-
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);
|
|
2415
2936
|
printStatus("progress", "check status anytime with `gnosys status --system`");
|
|
2416
2937
|
}
|
|
2417
2938
|
finally {
|
|
@@ -2474,29 +2995,18 @@ async function openRemoteDbIfConfigured(localDb) {
|
|
|
2474
2995
|
* Best-effort lookup of the API key for a provider. Used by the dream setup
|
|
2475
2996
|
* wizard to power the validation step. Mirrors the resolveApiKey precedence.
|
|
2476
2997
|
*/
|
|
2477
|
-
export async function getApiKeyForProvider(provider) {
|
|
2478
|
-
if (provider === "ollama" || provider === "lmstudio" || provider === "skip")
|
|
2998
|
+
export async function getApiKeyForProvider(provider, opts) {
|
|
2999
|
+
if (provider === "ollama" || provider === "lmstudio" || provider === "skip") {
|
|
2479
3000
|
return "";
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
};
|
|
2488
|
-
const fromEnv = process.env[envVarName] || (legacyVars[provider] && process.env[legacyVars[provider]]) || "";
|
|
2489
|
-
if (fromEnv)
|
|
2490
|
-
return fromEnv;
|
|
2491
|
-
if (process.platform === "darwin") {
|
|
2492
|
-
try {
|
|
2493
|
-
return execSync(`security find-generic-password -a "$USER" -s "${envVarName}" -w 2>/dev/null`, {
|
|
2494
|
-
stdio: "pipe", encoding: "utf-8", timeout: 2000,
|
|
2495
|
-
}).trim();
|
|
2496
|
-
}
|
|
2497
|
-
catch {
|
|
2498
|
-
// 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;
|
|
2499
3008
|
}
|
|
2500
3009
|
}
|
|
2501
|
-
return "";
|
|
3010
|
+
return readFirstInChain(provider) ?? "";
|
|
2502
3011
|
}
|
|
3012
|
+
export { getApiKeyForProviderFromConfig } from "./apiKeyVault.js";
|