gnosys 5.1.0 → 5.2.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 +166 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.js +151 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/multimodalIngest.d.ts.map +1 -1
- package/dist/lib/multimodalIngest.js +37 -9
- package/dist/lib/multimodalIngest.js.map +1 -1
- package/dist/lib/projectIdentity.d.ts +27 -0
- package/dist/lib/projectIdentity.d.ts.map +1 -1
- package/dist/lib/projectIdentity.js +198 -0
- package/dist/lib/projectIdentity.js.map +1 -1
- package/dist/lib/setup.d.ts +22 -1
- package/dist/lib/setup.d.ts.map +1 -1
- package/dist/lib/setup.js +581 -102
- package/dist/lib/setup.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/setup.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Gnosys Interactive Setup Wizard.
|
|
3
3
|
*
|
|
4
4
|
* Guides users through provider selection, model tier, API key storage,
|
|
5
|
-
*
|
|
5
|
+
* task model configuration, and IDE integration.
|
|
6
|
+
* Web knowledge base is set up separately via: gnosys web init
|
|
6
7
|
*
|
|
7
8
|
* Uses Node.js built-in readline/promises — no external dependencies.
|
|
8
9
|
*/
|
|
@@ -13,6 +14,7 @@ import fsSync from "fs";
|
|
|
13
14
|
import path from "path";
|
|
14
15
|
import os from "os";
|
|
15
16
|
import { execSync } from "child_process";
|
|
17
|
+
import { loadConfig, updateConfig, getProviderModel, } from "./config.js";
|
|
16
18
|
// ─── ANSI Colors ────────────────────────────────────────────────────────────
|
|
17
19
|
const BOLD = "\x1b[1m";
|
|
18
20
|
const DIM = "\x1b[2m";
|
|
@@ -275,6 +277,14 @@ const PROVIDER_ORDER = [
|
|
|
275
277
|
"lmstudio",
|
|
276
278
|
"custom",
|
|
277
279
|
];
|
|
280
|
+
// Task descriptions for display
|
|
281
|
+
const TASK_DESCRIPTIONS = {
|
|
282
|
+
structuring: "adding memories, tagging",
|
|
283
|
+
synthesis: "Q&A answers",
|
|
284
|
+
vision: "images, PDFs",
|
|
285
|
+
transcription: "audio files",
|
|
286
|
+
dream: "overnight consolidation",
|
|
287
|
+
};
|
|
278
288
|
// ─── Exported Helpers ───────────────────────────────────────────────────────
|
|
279
289
|
/**
|
|
280
290
|
* Returns the cheapest capable model for structuring tasks.
|
|
@@ -329,6 +339,89 @@ export async function writeApiKey(provider, key) {
|
|
|
329
339
|
}
|
|
330
340
|
await fs.writeFile(envPath, lines.join("\n") + "\n", "utf-8");
|
|
331
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Write an API key to the macOS Keychain.
|
|
344
|
+
* Uses the -U flag to update if the entry already exists.
|
|
345
|
+
* Returns true on success, false on failure.
|
|
346
|
+
*/
|
|
347
|
+
export function writeApiKeyToKeychain(envVar, key) {
|
|
348
|
+
if (process.platform !== "darwin")
|
|
349
|
+
return false;
|
|
350
|
+
try {
|
|
351
|
+
// The -U flag updates if the password already exists
|
|
352
|
+
execSync(`security add-generic-password -a "$USER" -s "${envVar}" -w "${key.replace(/"/g, '\\"')}" -U`, { stdio: "pipe" });
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Write an API key using Linux secret-tool (GNOME Keyring).
|
|
361
|
+
* Returns true on success, false on failure.
|
|
362
|
+
*/
|
|
363
|
+
function writeApiKeyToSecretTool(envVar, key, provider) {
|
|
364
|
+
if (process.platform === "darwin")
|
|
365
|
+
return false;
|
|
366
|
+
try {
|
|
367
|
+
// Check if secret-tool is available
|
|
368
|
+
execSync("which secret-tool", { stdio: "pipe" });
|
|
369
|
+
// Write the key — printf avoids trailing newline issues
|
|
370
|
+
execSync(`printf "%s" "${key.replace(/"/g, '\\"')}" | secret-tool store --label="Gnosys ${provider}" service gnosys account ${envVar}`, { stdio: "pipe", shell: "/bin/sh" });
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Check if secret-tool is available on Linux.
|
|
379
|
+
*/
|
|
380
|
+
function hasSecretTool() {
|
|
381
|
+
if (process.platform === "darwin")
|
|
382
|
+
return false;
|
|
383
|
+
try {
|
|
384
|
+
execSync("which secret-tool", { stdio: "pipe" });
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Detect where an existing API key is stored.
|
|
393
|
+
* Returns description like "macOS Keychain", "env var", ".env file", or empty string.
|
|
394
|
+
*/
|
|
395
|
+
function detectKeySource(envVarName, legacyEnvVar) {
|
|
396
|
+
// Check macOS Keychain
|
|
397
|
+
if (process.platform === "darwin") {
|
|
398
|
+
try {
|
|
399
|
+
const result = execSync(`security find-generic-password -a "$USER" -s "${envVarName}" -w`, { stdio: "pipe", encoding: "utf-8" }).trim();
|
|
400
|
+
if (result)
|
|
401
|
+
return "macOS Keychain";
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// Not in keychain
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Check environment variable (new-style)
|
|
408
|
+
if (process.env[envVarName])
|
|
409
|
+
return `$${envVarName}`;
|
|
410
|
+
// Check legacy env var
|
|
411
|
+
if (legacyEnvVar && process.env[legacyEnvVar])
|
|
412
|
+
return `$${legacyEnvVar}`;
|
|
413
|
+
// Check .env file
|
|
414
|
+
try {
|
|
415
|
+
const envPath = path.join(os.homedir(), ".config", "gnosys", ".env");
|
|
416
|
+
const content = fsSync.readFileSync(envPath, "utf-8");
|
|
417
|
+
if (content.includes(`${envVarName}=`))
|
|
418
|
+
return "~/.config/gnosys/.env";
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// No .env
|
|
422
|
+
}
|
|
423
|
+
return "";
|
|
424
|
+
}
|
|
332
425
|
/**
|
|
333
426
|
* Detect which IDEs are available in the given project directory.
|
|
334
427
|
* Returns an array like ["claude", "cursor", "codex"].
|
|
@@ -440,7 +533,8 @@ export async function setupIDE(ide, projectDir) {
|
|
|
440
533
|
*/
|
|
441
534
|
async function askChoice(rl, question, options) {
|
|
442
535
|
console.log();
|
|
443
|
-
|
|
536
|
+
if (question)
|
|
537
|
+
console.log(question);
|
|
444
538
|
console.log();
|
|
445
539
|
for (let i = 0; i < options.length; i++) {
|
|
446
540
|
console.log(` ${BOLD}${i + 1}.${RESET} ${options[i]}`);
|
|
@@ -485,6 +579,7 @@ function formatPrice(input, output) {
|
|
|
485
579
|
}
|
|
486
580
|
/**
|
|
487
581
|
* Print a bordered box with a title and key-value rows.
|
|
582
|
+
* Supports rows with empty keys (spacer rows) and section headers.
|
|
488
583
|
*/
|
|
489
584
|
function printBox(title, rows) {
|
|
490
585
|
const maxKeyLen = Math.max(...rows.map(([k]) => k.length));
|
|
@@ -497,8 +592,14 @@ function printBox(title, rows) {
|
|
|
497
592
|
console.log(`\u2502 ${BOLD}${title}${RESET}${" ".repeat(innerWidth - title.length - 2)}\u2502`);
|
|
498
593
|
console.log(`\u251C${border}\u2524`);
|
|
499
594
|
for (const [key, val] of rows) {
|
|
500
|
-
|
|
501
|
-
|
|
595
|
+
if (key === "" && val === "") {
|
|
596
|
+
// Spacer row
|
|
597
|
+
console.log(`\u2502${" ".repeat(innerWidth)}\u2502`);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
const line = `${key.padEnd(maxKeyLen)} ${val}`;
|
|
601
|
+
console.log(`\u2502 ${line}${" ".repeat(innerWidth - line.length - 2)}\u2502`);
|
|
602
|
+
}
|
|
502
603
|
}
|
|
503
604
|
console.log(`\u2514${border}\u2518`);
|
|
504
605
|
console.log();
|
|
@@ -535,6 +636,77 @@ async function getRegisteredProjects() {
|
|
|
535
636
|
}
|
|
536
637
|
return projects;
|
|
537
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Try to load existing gnosys.json config for displaying current values.
|
|
641
|
+
* Checks the project .gnosys dir first, then the global ~/.gnosys dir.
|
|
642
|
+
* Returns null if no config found.
|
|
643
|
+
*/
|
|
644
|
+
async function loadExistingConfig(projectDir) {
|
|
645
|
+
// Try project-level config first
|
|
646
|
+
try {
|
|
647
|
+
const projectStore = path.join(projectDir, ".gnosys");
|
|
648
|
+
const stat = await fs.stat(path.join(projectStore, "gnosys.json"));
|
|
649
|
+
if (stat.isFile()) {
|
|
650
|
+
return await loadConfig(projectStore);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// No project config
|
|
655
|
+
}
|
|
656
|
+
// Try global config at ~/.gnosys
|
|
657
|
+
try {
|
|
658
|
+
const globalStore = path.join(os.homedir(), ".gnosys");
|
|
659
|
+
const stat = await fs.stat(path.join(globalStore, "gnosys.json"));
|
|
660
|
+
if (stat.isFile()) {
|
|
661
|
+
return await loadConfig(globalStore);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
// No global config
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Let the user pick a provider from the list.
|
|
671
|
+
* Returns the provider name or "skip".
|
|
672
|
+
* If currentProvider is given, shows it as the current value.
|
|
673
|
+
*/
|
|
674
|
+
async function pickProvider(rl, dynamicModels, stepLabel, currentProvider) {
|
|
675
|
+
const currentHint = currentProvider ? ` ${DIM}(current: ${currentProvider})${RESET}` : "";
|
|
676
|
+
const providerOptions = PROVIDER_ORDER.map((key) => {
|
|
677
|
+
const tiers = dynamicModels[key] ?? PROVIDER_TIERS[key];
|
|
678
|
+
const display = PROVIDER_DISPLAY[key];
|
|
679
|
+
if (!tiers || tiers.length === 0)
|
|
680
|
+
return display;
|
|
681
|
+
const minIn = Math.min(...tiers.map((t) => t.input));
|
|
682
|
+
const maxOut = Math.max(...tiers.map((t) => t.output));
|
|
683
|
+
if (minIn === 0 && maxOut === 0)
|
|
684
|
+
return display;
|
|
685
|
+
return `${display} ${DIM}$${minIn.toFixed(2)}\u2013$${maxOut.toFixed(2)}/M tokens${RESET}`;
|
|
686
|
+
});
|
|
687
|
+
const choiceIdx = await askChoice(rl, `${stepLabel}${currentHint}`, providerOptions);
|
|
688
|
+
return PROVIDER_ORDER[choiceIdx];
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Let the user pick a model from a provider's tiers.
|
|
692
|
+
* Returns the model string.
|
|
693
|
+
*/
|
|
694
|
+
async function pickModel(rl, provider, dynamicModels, stepLabel, currentModel) {
|
|
695
|
+
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
696
|
+
if (!tiers || tiers.length === 0)
|
|
697
|
+
return "";
|
|
698
|
+
const isLocal = provider === "ollama" || provider === "lmstudio";
|
|
699
|
+
const currentHint = currentModel ? ` ${DIM}(current: ${currentModel})${RESET}` : "";
|
|
700
|
+
const tierOptions = tiers.map((t) => {
|
|
701
|
+
const rec = t.recommended ? ` ${CYAN}<- recommended${RESET}` : "";
|
|
702
|
+
if (isLocal) {
|
|
703
|
+
return `${t.name}${rec}`;
|
|
704
|
+
}
|
|
705
|
+
return `${t.name} (${t.model}) ${DIM}${formatPrice(t.input, t.output)}${RESET}${rec}`;
|
|
706
|
+
});
|
|
707
|
+
const tierIndex = await askChoice(rl, `${stepLabel}${currentHint}`, tierOptions);
|
|
708
|
+
return tiers[tierIndex].model;
|
|
709
|
+
}
|
|
538
710
|
// ─── Main Setup Wizard ──────────────────────────────────────────────────────
|
|
539
711
|
export async function runSetup(opts) {
|
|
540
712
|
const version = getVersion();
|
|
@@ -585,6 +757,12 @@ export async function runSetup(opts) {
|
|
|
585
757
|
console.log(`\u2502 ${DIM}${tagline}${RESET}${" ".repeat(bannerInner - tagline.length - 2)}\u2502`);
|
|
586
758
|
console.log(`\u2514${bannerBorder}\u2518`);
|
|
587
759
|
console.log();
|
|
760
|
+
// ─── Load existing config for defaults ───────────────────────────
|
|
761
|
+
const existingConfig = await loadExistingConfig(projectDir);
|
|
762
|
+
const currentProvider = existingConfig?.llm.defaultProvider;
|
|
763
|
+
const currentModel = existingConfig
|
|
764
|
+
? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
|
|
765
|
+
: undefined;
|
|
588
766
|
// ─── Pre-check: Upgrade detection ─────────────────────────────────
|
|
589
767
|
const centralDbPath = path.join(os.homedir(), ".gnosys", "gnosys.db");
|
|
590
768
|
const centralDbExists = fsSync.existsSync(centralDbPath);
|
|
@@ -643,7 +821,7 @@ export async function runSetup(opts) {
|
|
|
643
821
|
console.log(`${DIM}Using bundled model data (offline or fetch failed)${RESET}`);
|
|
644
822
|
}
|
|
645
823
|
console.log();
|
|
646
|
-
// ─── Step 1/
|
|
824
|
+
// ─── Step 1/5 — Provider ──────────────────────────────────────────
|
|
647
825
|
const providerOptions = PROVIDER_ORDER.map((key) => {
|
|
648
826
|
const tiers = dynamicModels[key] ?? PROVIDER_TIERS[key];
|
|
649
827
|
const display = PROVIDER_DISPLAY[key];
|
|
@@ -657,15 +835,21 @@ export async function runSetup(opts) {
|
|
|
657
835
|
});
|
|
658
836
|
// Add "Skip" option
|
|
659
837
|
providerOptions.push("Skip (core memory works without LLM)");
|
|
660
|
-
const
|
|
838
|
+
const currentProviderHint = currentProvider
|
|
839
|
+
? ` ${DIM}(current: ${currentProvider})${RESET}`
|
|
840
|
+
: "";
|
|
841
|
+
const providerIndex = await askChoice(rl, `${BOLD}Step 1/5${RESET} ${DIM}\u2014${RESET} Choose your LLM provider${currentProviderHint}`, providerOptions);
|
|
661
842
|
const isSkip = providerIndex === PROVIDER_ORDER.length; // last option
|
|
662
843
|
const provider = isSkip ? "skip" : PROVIDER_ORDER[providerIndex];
|
|
663
|
-
// ─── Step 2/
|
|
844
|
+
// ─── Step 2/5 — Model tier ────────────────────────────────────────
|
|
664
845
|
let model = "";
|
|
665
846
|
if (!isSkip && provider !== "custom") {
|
|
666
847
|
const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
|
|
667
848
|
if (tiers.length > 0) {
|
|
668
849
|
const isLocal = provider === "ollama" || provider === "lmstudio";
|
|
850
|
+
const currentModelHint = (currentProvider === provider && currentModel)
|
|
851
|
+
? ` ${DIM}(current: ${currentModel})${RESET}`
|
|
852
|
+
: "";
|
|
669
853
|
const tierOptions = tiers.map((t) => {
|
|
670
854
|
const rec = t.recommended ? ` ${CYAN}<- recommended${RESET}` : "";
|
|
671
855
|
if (isLocal) {
|
|
@@ -673,14 +857,14 @@ export async function runSetup(opts) {
|
|
|
673
857
|
}
|
|
674
858
|
return `${t.name} (${t.model}) ${DIM}${formatPrice(t.input, t.output)}${RESET}${rec}`;
|
|
675
859
|
});
|
|
676
|
-
const tierIndex = await askChoice(rl, `${BOLD}Step 2/
|
|
860
|
+
const tierIndex = await askChoice(rl, `${BOLD}Step 2/5${RESET} ${DIM}\u2014${RESET} Choose model tier${currentModelHint}`, tierOptions);
|
|
677
861
|
model = tiers[tierIndex].model;
|
|
678
862
|
}
|
|
679
863
|
}
|
|
680
864
|
else if (provider === "custom") {
|
|
681
865
|
// Custom: ask for base URL and model name
|
|
682
866
|
console.log();
|
|
683
|
-
console.log(`${BOLD}Step 2/
|
|
867
|
+
console.log(`${BOLD}Step 2/5${RESET} ${DIM}\u2014${RESET} Custom provider details`);
|
|
684
868
|
console.log();
|
|
685
869
|
const baseUrl = await askInput(rl, "Base URL (OpenAI-compatible)");
|
|
686
870
|
model = await askInput(rl, "Model name");
|
|
@@ -715,11 +899,11 @@ export async function runSetup(opts) {
|
|
|
715
899
|
}
|
|
716
900
|
}
|
|
717
901
|
else if (isSkip) {
|
|
718
|
-
// Skip step
|
|
902
|
+
// Skip step 2 entirely
|
|
719
903
|
console.log();
|
|
720
|
-
console.log(`${DIM}Step 2/
|
|
904
|
+
console.log(`${DIM}Step 2/5 \u2014 Model tier: skipped${RESET}`);
|
|
721
905
|
}
|
|
722
|
-
// ─── Step 3/
|
|
906
|
+
// ─── Step 3/5 — API key ───────────────────────────────────────────
|
|
723
907
|
let apiKeyWritten = false;
|
|
724
908
|
let apiKeySource = "";
|
|
725
909
|
const needsKey = !isSkip &&
|
|
@@ -739,57 +923,95 @@ export async function runSetup(opts) {
|
|
|
739
923
|
const legacyEnvVar = legacyEnvVars[provider] ?? "";
|
|
740
924
|
if (needsKey) {
|
|
741
925
|
console.log();
|
|
742
|
-
console.log(`${BOLD}Step 3/
|
|
926
|
+
console.log(`${BOLD}Step 3/5${RESET} ${DIM}\u2014${RESET} API Key`);
|
|
743
927
|
console.log();
|
|
928
|
+
// Check where the key currently lives
|
|
929
|
+
const existingKeySource = detectKeySource(envVarName, legacyEnvVar);
|
|
744
930
|
// Check if key already exists in environment
|
|
745
931
|
const existingKey = process.env[envVarName] || (legacyEnvVar ? process.env[legacyEnvVar] : "");
|
|
746
|
-
if (existingKey) {
|
|
747
|
-
const source =
|
|
748
|
-
console.log(` ${CHECK} Found existing key
|
|
932
|
+
if (existingKey || existingKeySource) {
|
|
933
|
+
const source = existingKeySource || "env";
|
|
934
|
+
console.log(` ${CHECK} Found existing key (${source})`);
|
|
935
|
+
if (existingKey) {
|
|
936
|
+
console.log(` ${DIM} ${maskKey(existingKey)}${RESET}`);
|
|
937
|
+
}
|
|
749
938
|
apiKeyWritten = true;
|
|
750
|
-
apiKeySource = "env";
|
|
939
|
+
apiKeySource = existingKeySource || "env";
|
|
940
|
+
// Offer to change it
|
|
941
|
+
const changeKey = await askYesNo(rl, " Change key storage?", false);
|
|
942
|
+
if (!changeKey) {
|
|
943
|
+
// Keep existing — skip the rest of step 3
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
// Fall through to key storage options below
|
|
947
|
+
apiKeyWritten = false;
|
|
948
|
+
apiKeySource = "";
|
|
949
|
+
}
|
|
751
950
|
}
|
|
752
|
-
|
|
951
|
+
if (!apiKeyWritten) {
|
|
753
952
|
console.log(` Provider: ${GREEN}${provider}${RESET}`);
|
|
754
953
|
console.log(` Env var: ${GREEN}${envVarName}${RESET}`);
|
|
755
954
|
console.log();
|
|
756
955
|
const isMac = process.platform === "darwin";
|
|
956
|
+
const isLinux = process.platform === "linux";
|
|
957
|
+
const hasSecret = isLinux && hasSecretTool();
|
|
757
958
|
const shell = path.basename(process.env.SHELL ?? "zsh");
|
|
758
959
|
const profileFile = shell === "bash" ? "~/.bash_profile" : "~/.zshrc";
|
|
759
960
|
const options = [];
|
|
760
961
|
if (isMac) {
|
|
761
962
|
options.push(`Store in macOS Keychain (recommended \u2014 most secure, no plaintext on disk)`);
|
|
762
963
|
}
|
|
964
|
+
if (hasSecret) {
|
|
965
|
+
options.push(`Store in GNOME Keyring (recommended \u2014 encrypted, no plaintext on disk)`);
|
|
966
|
+
}
|
|
763
967
|
options.push(`Set via environment variable (${profileFile})`, `Save to ~/.config/gnosys/.env (\u26a0 plaintext on disk \u2014 least secure)`, `Skip (configure later)`);
|
|
764
968
|
const keyChoice = await askChoice(rl, "", options);
|
|
765
|
-
//
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
const
|
|
769
|
-
const
|
|
969
|
+
// Build the index map based on which options are present
|
|
970
|
+
let idx = 0;
|
|
971
|
+
const keychainIdx = isMac ? idx++ : -1;
|
|
972
|
+
const gnomeIdx = hasSecret ? idx++ : -1;
|
|
973
|
+
const envIdx = idx++;
|
|
974
|
+
const dotenvIdx = idx++;
|
|
975
|
+
const skipIdx = idx++;
|
|
976
|
+
// skipIdx is unused as a variable but documents the last index
|
|
770
977
|
if (keyChoice === keychainIdx) {
|
|
771
|
-
// macOS Keychain
|
|
772
|
-
console.log();
|
|
773
|
-
console.log(` Run this in a ${BOLD}separate terminal${RESET}:`);
|
|
774
|
-
console.log();
|
|
775
|
-
console.log(` ${GREEN}security add-generic-password -a "$USER" -s "${envVarName}" -w "your-key-here"${RESET}`);
|
|
776
|
-
console.log();
|
|
777
|
-
console.log(` ${DIM}Replace "your-key-here" with your actual API key.${RESET}`);
|
|
778
|
-
console.log(` ${DIM}Gnosys will read it automatically at runtime.${RESET}`);
|
|
978
|
+
// macOS Keychain — ask for the key directly and write it
|
|
779
979
|
console.log();
|
|
780
|
-
await askInput(rl,
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (result) {
|
|
786
|
-
console.log(` ${CHECK} Key verified in macOS Keychain (${maskKey(result)})`);
|
|
980
|
+
const key = await askInput(rl, `Enter your ${provider} API key`);
|
|
981
|
+
if (key) {
|
|
982
|
+
const success = writeApiKeyToKeychain(envVarName, key);
|
|
983
|
+
if (success) {
|
|
984
|
+
console.log(` ${CHECK} Key saved to macOS Keychain (${maskKey(key)})`);
|
|
787
985
|
apiKeyWritten = true;
|
|
788
|
-
apiKeySource = "
|
|
986
|
+
apiKeySource = "macOS Keychain";
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
console.log(` ${CROSS} Failed to write to Keychain. Falling back to .env file.`);
|
|
990
|
+
await writeApiKey(provider, key);
|
|
991
|
+
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
|
|
992
|
+
apiKeyWritten = true;
|
|
993
|
+
apiKeySource = "~/.config/gnosys/.env";
|
|
789
994
|
}
|
|
790
995
|
}
|
|
791
|
-
|
|
792
|
-
|
|
996
|
+
}
|
|
997
|
+
else if (keyChoice === gnomeIdx) {
|
|
998
|
+
// Linux GNOME Keyring — ask for the key directly
|
|
999
|
+
console.log();
|
|
1000
|
+
const key = await askInput(rl, `Enter your ${provider} API key`);
|
|
1001
|
+
if (key) {
|
|
1002
|
+
const success = writeApiKeyToSecretTool(envVarName, key, provider);
|
|
1003
|
+
if (success) {
|
|
1004
|
+
console.log(` ${CHECK} Key saved to GNOME Keyring (${maskKey(key)})`);
|
|
1005
|
+
apiKeyWritten = true;
|
|
1006
|
+
apiKeySource = "GNOME Keyring";
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
console.log(` ${CROSS} Failed to write to GNOME Keyring. Falling back to .env file.`);
|
|
1010
|
+
await writeApiKey(provider, key);
|
|
1011
|
+
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
|
|
1012
|
+
apiKeyWritten = true;
|
|
1013
|
+
apiKeySource = "~/.config/gnosys/.env";
|
|
1014
|
+
}
|
|
793
1015
|
}
|
|
794
1016
|
}
|
|
795
1017
|
else if (keyChoice === envIdx) {
|
|
@@ -804,10 +1026,9 @@ export async function runSetup(opts) {
|
|
|
804
1026
|
console.log();
|
|
805
1027
|
await askInput(rl, "Press Enter after setting the key...", { default: "" });
|
|
806
1028
|
// Verify
|
|
807
|
-
// Note: we can't detect it in this process since env was set in another terminal
|
|
808
1029
|
console.log(` ${DIM}Key will be available in new terminal sessions.${RESET}`);
|
|
809
1030
|
apiKeyWritten = true;
|
|
810
|
-
apiKeySource = "
|
|
1031
|
+
apiKeySource = "shell profile";
|
|
811
1032
|
}
|
|
812
1033
|
else if (keyChoice === dotenvIdx) {
|
|
813
1034
|
// .env file (least secure)
|
|
@@ -824,7 +1045,7 @@ export async function runSetup(opts) {
|
|
|
824
1045
|
await writeApiKey(provider, key);
|
|
825
1046
|
console.log(` ${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
|
|
826
1047
|
apiKeyWritten = true;
|
|
827
|
-
apiKeySource = "
|
|
1048
|
+
apiKeySource = "~/.config/gnosys/.env";
|
|
828
1049
|
}
|
|
829
1050
|
}
|
|
830
1051
|
else {
|
|
@@ -834,7 +1055,12 @@ export async function runSetup(opts) {
|
|
|
834
1055
|
else {
|
|
835
1056
|
// Skip
|
|
836
1057
|
console.log(` ${DIM}Skipped. Set your key later using one of these methods:`);
|
|
837
|
-
|
|
1058
|
+
if (isMac) {
|
|
1059
|
+
console.log(` \u2022 macOS Keychain: security add-generic-password -a "$USER" -s "${envVarName}" -w "key" -U`);
|
|
1060
|
+
}
|
|
1061
|
+
if (hasSecret) {
|
|
1062
|
+
console.log(` \u2022 GNOME Keyring: printf '%s' 'key' | secret-tool store --label="Gnosys ${provider}" service gnosys account ${envVarName}`);
|
|
1063
|
+
}
|
|
838
1064
|
console.log(` \u2022 Shell profile: echo 'export ${envVarName}=key' >> ${profileFile}`);
|
|
839
1065
|
console.log(` \u2022 Dotenv file: echo '${envVarName}=key' >> ~/.config/gnosys/.env${RESET}`);
|
|
840
1066
|
}
|
|
@@ -842,84 +1068,335 @@ export async function runSetup(opts) {
|
|
|
842
1068
|
}
|
|
843
1069
|
else {
|
|
844
1070
|
console.log();
|
|
845
|
-
console.log(`${DIM}Step 3/
|
|
1071
|
+
console.log(`${DIM}Step 3/5 \u2014 API key: not needed (local provider)${RESET}`);
|
|
846
1072
|
}
|
|
847
|
-
// ─── Step 4/
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
cursor: "Cursor",
|
|
854
|
-
codex: "Codex",
|
|
855
|
-
};
|
|
856
|
-
const detectedNames = detectedIdes.map((id) => ideLabels[id] ?? id).join(", ");
|
|
1073
|
+
// ─── Step 4/5 — Task Model Configuration ─────────────────────────
|
|
1074
|
+
const taskOverrides = {};
|
|
1075
|
+
let dreamEnabled = existingConfig?.dream?.enabled ?? false;
|
|
1076
|
+
let dreamProvider = existingConfig?.dream?.provider ?? "ollama";
|
|
1077
|
+
let dreamModel = existingConfig?.dream?.model ?? "";
|
|
1078
|
+
if (!isSkip) {
|
|
857
1079
|
console.log();
|
|
858
|
-
console.log(`${BOLD}Step 4/
|
|
1080
|
+
console.log(`${BOLD}Step 4/5${RESET} ${DIM}\u2014${RESET} Task Routing`);
|
|
859
1081
|
console.log();
|
|
860
|
-
console.log(`
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
idesToSetup = [detectedIdes[ideIndex]];
|
|
874
|
-
}
|
|
875
|
-
else if (detectedIdes.length > 1 && ideIndex === detectedIdes.length) {
|
|
876
|
-
// "All detected"
|
|
877
|
-
idesToSetup = [...detectedIdes];
|
|
878
|
-
}
|
|
879
|
-
// Last option is always "Skip"
|
|
880
|
-
for (const ide of idesToSetup) {
|
|
881
|
-
const result = await setupIDE(ide, projectDir);
|
|
882
|
-
if (result.success) {
|
|
883
|
-
console.log(` ${CHECK} ${result.message}`);
|
|
884
|
-
configuredIdes.push(ide);
|
|
1082
|
+
console.log(`Gnosys uses different LLM models for different tasks. Each defaults to your`);
|
|
1083
|
+
console.log(`chosen provider, but you can override them individually.`);
|
|
1084
|
+
console.log();
|
|
1085
|
+
const tasks = ["structuring", "synthesis", "vision", "transcription"];
|
|
1086
|
+
// Build a temporary config to see what defaults look like with the new provider
|
|
1087
|
+
const effectiveRouting = {};
|
|
1088
|
+
for (const task of tasks) {
|
|
1089
|
+
if (existingConfig?.taskModels?.[task]) {
|
|
1090
|
+
// Use the existing override
|
|
1091
|
+
effectiveRouting[task] = {
|
|
1092
|
+
provider: existingConfig.taskModels[task].provider,
|
|
1093
|
+
model: existingConfig.taskModels[task].model,
|
|
1094
|
+
};
|
|
885
1095
|
}
|
|
886
1096
|
else {
|
|
887
|
-
|
|
1097
|
+
// Derive from the newly chosen default provider
|
|
1098
|
+
const p = provider;
|
|
1099
|
+
let m = model;
|
|
1100
|
+
if (task === "structuring") {
|
|
1101
|
+
m = getStructuringModel(p, model);
|
|
1102
|
+
}
|
|
1103
|
+
effectiveRouting[task] = { provider: p, model: m };
|
|
888
1104
|
}
|
|
889
1105
|
}
|
|
890
|
-
//
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1106
|
+
// Dream routing
|
|
1107
|
+
effectiveRouting.dream = {
|
|
1108
|
+
provider: dreamProvider,
|
|
1109
|
+
model: dreamModel || getProviderModel(existingConfig ?? { llm: { defaultProvider: "ollama", ollama: { model: "llama3.2", baseUrl: "http://localhost:11434" } } }, dreamProvider),
|
|
1110
|
+
};
|
|
1111
|
+
// Display the table
|
|
1112
|
+
const taskNameWidth = 16;
|
|
1113
|
+
const routingWidth = 38;
|
|
1114
|
+
console.log(` ${BOLD}${"Task".padEnd(taskNameWidth)}${"Current Routing".padEnd(routingWidth)}${RESET}`);
|
|
1115
|
+
console.log(` ${"\u2500".repeat(taskNameWidth + routingWidth)}`);
|
|
1116
|
+
for (const task of [...tasks, "dream"]) {
|
|
1117
|
+
const r = effectiveRouting[task];
|
|
1118
|
+
const desc = TASK_DESCRIPTIONS[task] ?? "";
|
|
1119
|
+
const routingStr = `${r.provider} / ${r.model}`;
|
|
1120
|
+
const status = task === "dream" && !dreamEnabled ? `${DIM}(disabled)${RESET}` : `${DIM}(${desc})${RESET}`;
|
|
1121
|
+
console.log(` ${task.padEnd(taskNameWidth)}${routingStr.padEnd(routingWidth)}${status}`);
|
|
1122
|
+
}
|
|
1123
|
+
console.log();
|
|
1124
|
+
const taskChoice = await askChoice(rl, "", [
|
|
1125
|
+
`Keep defaults (use ${provider} for everything available)`,
|
|
1126
|
+
"Customize individual tasks",
|
|
1127
|
+
"Use same provider for ALL tasks (including dream)",
|
|
1128
|
+
]);
|
|
1129
|
+
if (taskChoice === 1) {
|
|
1130
|
+
// Customize individual tasks
|
|
1131
|
+
console.log();
|
|
1132
|
+
console.log(`${DIM}For each task, pick a provider and model. Press Enter to keep the default.${RESET}`);
|
|
1133
|
+
for (const task of tasks) {
|
|
1134
|
+
console.log();
|
|
1135
|
+
console.log(` ${BOLD}${task}${RESET} ${DIM}(${TASK_DESCRIPTIONS[task]})${RESET}`);
|
|
1136
|
+
const currentTaskRouting = effectiveRouting[task];
|
|
1137
|
+
const useDefault = await askYesNo(rl, ` Keep ${currentTaskRouting.provider} / ${currentTaskRouting.model}?`, true);
|
|
1138
|
+
if (!useDefault) {
|
|
1139
|
+
// Pick a provider for this task
|
|
1140
|
+
const taskProvider = await pickProvider(rl, dynamicModels, ` Provider for ${task}`, currentTaskRouting.provider);
|
|
1141
|
+
// Pick a model
|
|
1142
|
+
let taskModel;
|
|
1143
|
+
if (taskProvider === "ollama" || taskProvider === "lmstudio") {
|
|
1144
|
+
taskModel = await pickModel(rl, taskProvider, dynamicModels, ` Model for ${task}`);
|
|
1145
|
+
}
|
|
1146
|
+
else if (taskProvider === "custom") {
|
|
1147
|
+
taskModel = await askInput(rl, " Model name");
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
taskModel = await pickModel(rl, taskProvider, dynamicModels, ` Model for ${task}`);
|
|
1151
|
+
}
|
|
1152
|
+
taskOverrides[task] = { provider: taskProvider, model: taskModel };
|
|
1153
|
+
}
|
|
898
1154
|
}
|
|
899
|
-
|
|
900
|
-
|
|
1155
|
+
// Dream configuration
|
|
1156
|
+
console.log();
|
|
1157
|
+
console.log(` ${BOLD}dream${RESET} ${DIM}(${TASK_DESCRIPTIONS.dream})${RESET}`);
|
|
1158
|
+
console.log(` ${DIM}Dream mode runs offline consolidation — discovering relationships,`);
|
|
1159
|
+
console.log(` generating summaries, and scoring memories. Defaults to Ollama (free/local).${RESET}`);
|
|
1160
|
+
dreamEnabled = await askYesNo(rl, ` Enable dream mode?`, dreamEnabled);
|
|
1161
|
+
if (dreamEnabled) {
|
|
1162
|
+
const keepDreamDefault = await askYesNo(rl, ` Keep ${dreamProvider} / ${dreamModel || "default"}?`, true);
|
|
1163
|
+
if (!keepDreamDefault) {
|
|
1164
|
+
dreamProvider = await pickProvider(rl, dynamicModels, ` Provider for dream`, dreamProvider);
|
|
1165
|
+
dreamModel = await pickModel(rl, dreamProvider, dynamicModels, ` Model for dream`);
|
|
1166
|
+
}
|
|
1167
|
+
taskOverrides.dream = { provider: dreamProvider, model: dreamModel };
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
else if (taskChoice === 2) {
|
|
1171
|
+
// Use same provider for ALL tasks including dream
|
|
1172
|
+
console.log();
|
|
1173
|
+
console.log(` ${DIM}All tasks will use ${provider} / ${model}.${RESET}`);
|
|
1174
|
+
for (const task of tasks) {
|
|
1175
|
+
let taskModel = model;
|
|
1176
|
+
if (task === "structuring") {
|
|
1177
|
+
taskModel = getStructuringModel(provider, model);
|
|
1178
|
+
}
|
|
1179
|
+
taskOverrides[task] = { provider, model: taskModel };
|
|
1180
|
+
}
|
|
1181
|
+
dreamEnabled = await askYesNo(rl, ` Enable dream mode with ${provider}?`, true);
|
|
1182
|
+
if (dreamEnabled) {
|
|
1183
|
+
dreamProvider = provider;
|
|
1184
|
+
dreamModel = model;
|
|
1185
|
+
taskOverrides.dream = { provider, model };
|
|
901
1186
|
}
|
|
902
1187
|
}
|
|
1188
|
+
// taskChoice === 0: keep defaults, do nothing
|
|
903
1189
|
}
|
|
904
1190
|
else {
|
|
905
1191
|
console.log();
|
|
906
|
-
console.log(`${DIM}Step 4/
|
|
1192
|
+
console.log(`${DIM}Step 4/5 \u2014 Task routing: skipped (no provider)${RESET}`);
|
|
1193
|
+
}
|
|
1194
|
+
// ─── Step 5/5 — IDE integration (enhanced) ───────────────────────
|
|
1195
|
+
const detectedIdes = await detectIDEs(projectDir);
|
|
1196
|
+
const configuredIdes = [];
|
|
1197
|
+
console.log();
|
|
1198
|
+
console.log(`${BOLD}Step 5/5${RESET} ${DIM}\u2014${RESET} IDE Integration`);
|
|
1199
|
+
console.log();
|
|
1200
|
+
const ideLabels = {
|
|
1201
|
+
claude: "Claude Code",
|
|
1202
|
+
cursor: "Cursor",
|
|
1203
|
+
codex: "Codex",
|
|
1204
|
+
};
|
|
1205
|
+
// Build IDE options: show detected ones and offer to create missing ones
|
|
1206
|
+
const allIdeKeys = ["claude", "cursor", "codex"];
|
|
1207
|
+
const ideOptions = [];
|
|
1208
|
+
const ideKeyForOption = []; // parallel array mapping option index to IDE key
|
|
1209
|
+
for (const ide of allIdeKeys) {
|
|
1210
|
+
const isDetected = detectedIdes.includes(ide);
|
|
1211
|
+
const label = ideLabels[ide] ?? ide;
|
|
1212
|
+
if (isDetected) {
|
|
1213
|
+
ideOptions.push(`${label} (detected)`);
|
|
1214
|
+
}
|
|
1215
|
+
else if (ide === "claude") {
|
|
1216
|
+
// Claude CLI needs to be installed, can't just create a directory
|
|
1217
|
+
ideOptions.push(`${label} ${DIM}(not detected \u2014 install Claude CLI first)${RESET}`);
|
|
1218
|
+
}
|
|
1219
|
+
else {
|
|
1220
|
+
// Offer to create the directory
|
|
1221
|
+
ideOptions.push(`${label} ${DIM}(create .${ide}/ \u2014 not detected)${RESET}`);
|
|
1222
|
+
}
|
|
1223
|
+
ideKeyForOption.push(ide);
|
|
1224
|
+
}
|
|
1225
|
+
ideOptions.push("All");
|
|
1226
|
+
ideOptions.push("Skip");
|
|
1227
|
+
if (detectedIdes.length > 0) {
|
|
1228
|
+
const detectedNames = detectedIdes.map((id) => ideLabels[id] ?? id).join(", ");
|
|
1229
|
+
console.log(`Detected: ${GREEN}${detectedNames}${RESET}`);
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
console.log(`${DIM}No IDE integrations detected in this directory.${RESET}`);
|
|
1233
|
+
}
|
|
1234
|
+
const ideIndex = await askChoice(rl, "", ideOptions);
|
|
1235
|
+
let idesToSetup = [];
|
|
1236
|
+
if (ideIndex < allIdeKeys.length) {
|
|
1237
|
+
// Individual IDE selected
|
|
1238
|
+
idesToSetup = [ideKeyForOption[ideIndex]];
|
|
1239
|
+
}
|
|
1240
|
+
else if (ideIndex === allIdeKeys.length) {
|
|
1241
|
+
// "All"
|
|
1242
|
+
idesToSetup = [...allIdeKeys];
|
|
1243
|
+
}
|
|
1244
|
+
// Last option is "Skip"
|
|
1245
|
+
for (const ide of idesToSetup) {
|
|
1246
|
+
// For non-detected IDEs (except claude), create the directory first
|
|
1247
|
+
if (!detectedIdes.includes(ide) && ide !== "claude") {
|
|
1248
|
+
const dirPath = path.join(projectDir, `.${ide}`);
|
|
1249
|
+
try {
|
|
1250
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
1251
|
+
console.log(` ${CHECK} Created .${ide}/ directory`);
|
|
1252
|
+
}
|
|
1253
|
+
catch (err) {
|
|
1254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1255
|
+
console.log(` ${CROSS} Could not create .${ide}/: ${msg}`);
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
const result = await setupIDE(ide, projectDir);
|
|
1260
|
+
if (result.success) {
|
|
1261
|
+
console.log(` ${CHECK} ${result.message}`);
|
|
1262
|
+
configuredIdes.push(ide);
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
console.log(` ${CROSS} ${ideLabels[ide] ?? ide}: ${result.message}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
// Sync global rules
|
|
1269
|
+
if (idesToSetup.length > 0) {
|
|
1270
|
+
try {
|
|
1271
|
+
const { syncToTarget } = await import("./rulesGen.js");
|
|
1272
|
+
const { GnosysDB } = await import("./db.js");
|
|
1273
|
+
const db = GnosysDB.openCentral();
|
|
1274
|
+
await syncToTarget(db, projectDir, "global", null);
|
|
1275
|
+
db.close();
|
|
1276
|
+
}
|
|
1277
|
+
catch {
|
|
1278
|
+
// Non-critical — rules sync is best-effort during setup
|
|
1279
|
+
}
|
|
907
1280
|
}
|
|
908
1281
|
// ─── Compute structuring model ────────────────────────────────────
|
|
909
|
-
const structuringModel = isSkip ? "" : getStructuringModel(provider, model);
|
|
1282
|
+
const structuringModel = isSkip ? "" : (taskOverrides.structuring?.model ?? getStructuringModel(provider, model));
|
|
1283
|
+
// ─── Write config to gnosys.json ─────────────────────────────────
|
|
1284
|
+
if (!isSkip) {
|
|
1285
|
+
// Determine which store path to write to — prefer project, fall back to global
|
|
1286
|
+
let storePath;
|
|
1287
|
+
const projectStore = path.join(projectDir, ".gnosys");
|
|
1288
|
+
const globalStore = path.join(os.homedir(), ".gnosys");
|
|
1289
|
+
if (fsSync.existsSync(path.join(projectStore, "gnosys.json"))) {
|
|
1290
|
+
storePath = projectStore;
|
|
1291
|
+
}
|
|
1292
|
+
else if (fsSync.existsSync(path.join(globalStore, "gnosys.json"))) {
|
|
1293
|
+
storePath = globalStore;
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
// Default to global store — create directory if needed
|
|
1297
|
+
await fs.mkdir(globalStore, { recursive: true });
|
|
1298
|
+
storePath = globalStore;
|
|
1299
|
+
}
|
|
1300
|
+
// Build the config updates
|
|
1301
|
+
// Build LLM config update, preserving existing provider-specific settings
|
|
1302
|
+
const existingLlm = existingConfig?.llm;
|
|
1303
|
+
const existingProviderConfig = existingLlm
|
|
1304
|
+
? existingLlm[provider]
|
|
1305
|
+
: undefined;
|
|
1306
|
+
const providerConfigBase = (typeof existingProviderConfig === "object" && existingProviderConfig !== null)
|
|
1307
|
+
? existingProviderConfig
|
|
1308
|
+
: {};
|
|
1309
|
+
const configUpdates = {
|
|
1310
|
+
llm: {
|
|
1311
|
+
...(existingLlm ?? {}),
|
|
1312
|
+
defaultProvider: provider,
|
|
1313
|
+
[provider]: {
|
|
1314
|
+
...providerConfigBase,
|
|
1315
|
+
model,
|
|
1316
|
+
},
|
|
1317
|
+
},
|
|
1318
|
+
};
|
|
1319
|
+
// Task model overrides — only write if the user actually changed them
|
|
1320
|
+
const taskModelsUpdate = {};
|
|
1321
|
+
if (taskOverrides.structuring) {
|
|
1322
|
+
taskModelsUpdate.structuring = taskOverrides.structuring;
|
|
1323
|
+
}
|
|
1324
|
+
if (taskOverrides.synthesis) {
|
|
1325
|
+
taskModelsUpdate.synthesis = taskOverrides.synthesis;
|
|
1326
|
+
}
|
|
1327
|
+
if (taskOverrides.vision) {
|
|
1328
|
+
taskModelsUpdate.vision = taskOverrides.vision;
|
|
1329
|
+
}
|
|
1330
|
+
if (taskOverrides.transcription) {
|
|
1331
|
+
taskModelsUpdate.transcription = taskOverrides.transcription;
|
|
1332
|
+
}
|
|
1333
|
+
if (Object.keys(taskModelsUpdate).length > 0) {
|
|
1334
|
+
configUpdates.taskModels = taskModelsUpdate;
|
|
1335
|
+
}
|
|
1336
|
+
// Dream configuration
|
|
1337
|
+
configUpdates.dream = {
|
|
1338
|
+
...(existingConfig?.dream ?? {}),
|
|
1339
|
+
enabled: dreamEnabled,
|
|
1340
|
+
provider: dreamProvider,
|
|
1341
|
+
...(dreamModel ? { model: dreamModel } : {}),
|
|
1342
|
+
};
|
|
1343
|
+
try {
|
|
1344
|
+
await updateConfig(storePath, configUpdates);
|
|
1345
|
+
console.log();
|
|
1346
|
+
console.log(` ${CHECK} Config written to ${storePath}/gnosys.json`);
|
|
1347
|
+
}
|
|
1348
|
+
catch (err) {
|
|
1349
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1350
|
+
console.log();
|
|
1351
|
+
console.log(` ${WARN} Could not write config: ${msg}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
910
1354
|
// ─── Summary ──────────────────────────────────────────────────────
|
|
1355
|
+
// Compute final effective routing for summary display
|
|
1356
|
+
const summaryRouting = {};
|
|
1357
|
+
const taskNames = ["structuring", "synthesis", "vision", "transcription", "dream"];
|
|
1358
|
+
for (const task of taskNames) {
|
|
1359
|
+
if (isSkip) {
|
|
1360
|
+
summaryRouting[task] = "not configured";
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
if (task === "dream") {
|
|
1364
|
+
if (!dreamEnabled) {
|
|
1365
|
+
summaryRouting[task] = "disabled";
|
|
1366
|
+
}
|
|
1367
|
+
else {
|
|
1368
|
+
const p = taskOverrides.dream?.provider ?? dreamProvider;
|
|
1369
|
+
const m = taskOverrides.dream?.model ?? (dreamModel || "default");
|
|
1370
|
+
summaryRouting[task] = `${p} / ${m}`;
|
|
1371
|
+
}
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
if (taskOverrides[task]) {
|
|
1375
|
+
summaryRouting[task] = `${taskOverrides[task].provider} / ${taskOverrides[task].model}`;
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
// Default routing
|
|
1379
|
+
const p = provider;
|
|
1380
|
+
let m = model;
|
|
1381
|
+
if (task === "structuring")
|
|
1382
|
+
m = getStructuringModel(p, m);
|
|
1383
|
+
summaryRouting[task] = `${p} / ${m}`;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
911
1386
|
const summaryRows = [
|
|
912
1387
|
["Provider:", isSkip ? "none" : provider],
|
|
913
1388
|
["Model:", model || "none"],
|
|
914
|
-
["
|
|
915
|
-
["
|
|
1389
|
+
["API key:", apiKeyWritten ? `${apiKeySource} ${CHECK}` : "not set"],
|
|
1390
|
+
["", ""],
|
|
1391
|
+
["Task Routing:", ""],
|
|
1392
|
+
[" structuring:", summaryRouting.structuring],
|
|
1393
|
+
[" synthesis:", summaryRouting.synthesis],
|
|
1394
|
+
[" vision:", summaryRouting.vision],
|
|
1395
|
+
[" transcription:", summaryRouting.transcription],
|
|
1396
|
+
[" dream:", summaryRouting.dream],
|
|
916
1397
|
];
|
|
917
1398
|
if (configuredIdes.length > 0) {
|
|
918
|
-
|
|
919
|
-
claude: "Claude Code",
|
|
920
|
-
cursor: "Cursor",
|
|
921
|
-
codex: "Codex",
|
|
922
|
-
};
|
|
1399
|
+
summaryRows.push(["", ""]);
|
|
923
1400
|
const ideNames = configuredIdes.map((id) => ideLabels[id] ?? id).join(", ");
|
|
924
1401
|
summaryRows.push(["IDEs:", ideNames]);
|
|
925
1402
|
}
|
|
@@ -936,6 +1413,8 @@ export async function runSetup(opts) {
|
|
|
936
1413
|
ides: configuredIdes,
|
|
937
1414
|
mode: "agent",
|
|
938
1415
|
upgraded,
|
|
1416
|
+
taskOverrides: Object.keys(taskOverrides).length > 0 ? taskOverrides : undefined,
|
|
1417
|
+
dreamEnabled,
|
|
939
1418
|
};
|
|
940
1419
|
}
|
|
941
1420
|
catch (err) {
|