groove-dev 0.27.69 → 0.27.71
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/MOE_TRAINING_PIPELINE.md +720 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +272 -2
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/providers/base.js +8 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +52 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +15 -0
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +36 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-74E3YTkT.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DhnTm_1P.js → index-BK6tvmxx.js} +1739 -1738
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +1 -0
- package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -2
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +258 -84
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +272 -2
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/providers/base.js +8 -0
- package/packages/daemon/src/providers/claude-code.js +52 -0
- package/packages/daemon/src/providers/codex.js +15 -0
- package/packages/daemon/src/providers/gemini.js +14 -0
- package/packages/daemon/src/providers/index.js +36 -0
- package/packages/gui/dist/assets/index-74E3YTkT.css +1 -0
- package/packages/gui/dist/assets/{index-DhnTm_1P.js → index-BK6tvmxx.js} +1739 -1738
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/editor/code-editor.jsx +5 -5
- package/packages/gui/src/components/editor/editor-tabs.jsx +4 -4
- package/packages/gui/src/components/network/activity-chart.jsx +1 -0
- package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
- package/packages/gui/src/stores/groove.js +107 -2
- package/packages/gui/src/views/settings.jsx +258 -84
- package/node_modules/@groove-dev/gui/dist/assets/index-oQ0ejlfH.css +0 -1
- package/packages/gui/dist/assets/index-oQ0ejlfH.css +0 -1
|
@@ -10,7 +10,7 @@ import { createHash, randomUUID } from 'crypto';
|
|
|
10
10
|
import { hostname, networkInterfaces, homedir } from 'os';
|
|
11
11
|
import { StringDecoder } from 'string_decoder';
|
|
12
12
|
import { lookup as mimeLookup } from './mimetypes.js';
|
|
13
|
-
import { listProviders, getProvider } from './providers/index.js';
|
|
13
|
+
import { listProviders, getProvider, clearInstallCache, getProviderMetadata, getProviderPath, setProviderPaths } from './providers/index.js';
|
|
14
14
|
import { OllamaProvider } from './providers/ollama.js';
|
|
15
15
|
import { ClaudeCodeProvider } from './providers/claude-code.js';
|
|
16
16
|
import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
|
|
@@ -441,12 +441,18 @@ export function createApi(app, daemon) {
|
|
|
441
441
|
// List available providers
|
|
442
442
|
app.get('/api/providers', (req, res) => {
|
|
443
443
|
const providers = listProviders();
|
|
444
|
-
// Enrich with credential status
|
|
445
444
|
for (const p of providers) {
|
|
446
445
|
p.hasKey = daemon.credentials.hasKey(p.id);
|
|
447
446
|
if (p.id === 'claude-code') {
|
|
448
447
|
p.authStatus = ClaudeCodeProvider.getAuthStatus();
|
|
449
448
|
}
|
|
449
|
+
const meta = getProviderMetadata(p.id);
|
|
450
|
+
if (meta) {
|
|
451
|
+
p.setupGuide = meta.setupGuide;
|
|
452
|
+
p.authMethods = meta.authMethods;
|
|
453
|
+
}
|
|
454
|
+
const customPath = getProviderPath(p.id);
|
|
455
|
+
if (customPath) p.providerPath = customPath;
|
|
450
456
|
}
|
|
451
457
|
res.json(providers);
|
|
452
458
|
});
|
|
@@ -576,6 +582,268 @@ export function createApi(app, daemon) {
|
|
|
576
582
|
}
|
|
577
583
|
});
|
|
578
584
|
|
|
585
|
+
// --- Provider Management (install, login, set-path, verify) ---
|
|
586
|
+
|
|
587
|
+
const MANAGEABLE_PROVIDERS = new Set(['claude-code', 'codex', 'gemini']);
|
|
588
|
+
|
|
589
|
+
app.post('/api/providers/:id/install', (req, res) => {
|
|
590
|
+
const { id } = req.params;
|
|
591
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
592
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const INSTALL_PACKAGES = {
|
|
596
|
+
'claude-code': '@anthropic-ai/claude-code',
|
|
597
|
+
'codex': '@openai/codex',
|
|
598
|
+
'gemini': '@google/gemini-cli',
|
|
599
|
+
};
|
|
600
|
+
const pkg = INSTALL_PACKAGES[id];
|
|
601
|
+
|
|
602
|
+
res.setHeader('Content-Type', 'application/x-ndjson');
|
|
603
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
604
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
605
|
+
|
|
606
|
+
const write = (obj) => {
|
|
607
|
+
try { res.write(JSON.stringify(obj) + '\n'); } catch { /* client disconnected */ }
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
write({ status: 'installing', output: `Installing ${pkg}...`, progress: 0 });
|
|
611
|
+
|
|
612
|
+
const proc = spawn('npm', ['install', '-g', pkg], {
|
|
613
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
614
|
+
env: { ...process.env, NODE_ENV: undefined },
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
let output = '';
|
|
618
|
+
let errOutput = '';
|
|
619
|
+
|
|
620
|
+
proc.stdout.on('data', (data) => {
|
|
621
|
+
output += data.toString();
|
|
622
|
+
write({ status: 'installing', output: data.toString().trim(), progress: 50 });
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
proc.stderr.on('data', (data) => {
|
|
626
|
+
errOutput += data.toString();
|
|
627
|
+
const line = data.toString().trim();
|
|
628
|
+
if (line) write({ status: 'installing', output: line, progress: 50 });
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
proc.on('close', (code) => {
|
|
632
|
+
clearInstallCache();
|
|
633
|
+
const providerObj = getProvider(id);
|
|
634
|
+
const installed = providerObj ? providerObj.constructor.isInstalled() : false;
|
|
635
|
+
|
|
636
|
+
if (code === 0 && installed) {
|
|
637
|
+
write({ status: 'complete', output: `${pkg} installed successfully`, progress: 100, installed: true });
|
|
638
|
+
daemon.audit.log('provider.install', { provider: id, pkg, success: true });
|
|
639
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
640
|
+
} else {
|
|
641
|
+
const reason = code !== 0
|
|
642
|
+
? (errOutput || output).slice(-500)
|
|
643
|
+
: 'Install succeeded but provider binary not found in PATH';
|
|
644
|
+
write({ status: 'error', output: reason, progress: 100, installed: false });
|
|
645
|
+
daemon.audit.log('provider.install', { provider: id, pkg, success: false, code });
|
|
646
|
+
}
|
|
647
|
+
res.end();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
proc.on('error', (err) => {
|
|
651
|
+
write({ status: 'error', output: `Failed to start npm: ${err.message}`, progress: 100, installed: false });
|
|
652
|
+
res.end();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
req.on('close', () => {
|
|
656
|
+
try { proc.kill(); } catch { /* already exited */ }
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
app.post('/api/providers/:id/login', async (req, res) => {
|
|
661
|
+
const { id } = req.params;
|
|
662
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
663
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (id === 'gemini') {
|
|
667
|
+
return res.json({ status: 'not-supported', message: 'Gemini uses API key authentication. Set your key in Settings.' });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (id === 'claude-code') {
|
|
671
|
+
const providerObj = getProvider(id);
|
|
672
|
+
if (!providerObj || !providerObj.constructor.isInstalled()) {
|
|
673
|
+
return res.status(400).json({ error: 'Claude Code is not installed. Install it first.' });
|
|
674
|
+
}
|
|
675
|
+
daemon.audit.log('provider.login.started', { provider: id });
|
|
676
|
+
try {
|
|
677
|
+
const result = await ClaudeCodeProvider.startLogin();
|
|
678
|
+
clearInstallCache();
|
|
679
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
680
|
+
return res.json(result);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
return res.status(500).json({ status: 'error', error: err.message });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (id === 'codex') {
|
|
687
|
+
const providerObj = getProvider(id);
|
|
688
|
+
if (!providerObj || !providerObj.constructor.isInstalled()) {
|
|
689
|
+
return res.status(400).json({ error: 'Codex is not installed. Install it first.' });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const { method, key } = req.body || {};
|
|
693
|
+
|
|
694
|
+
if (key) {
|
|
695
|
+
daemon.audit.log('provider.login.started', { provider: id, method: 'api-key' });
|
|
696
|
+
try {
|
|
697
|
+
const result = await providerObj.constructor.onKeySet(key);
|
|
698
|
+
clearInstallCache();
|
|
699
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
700
|
+
return res.json({ status: result.ok ? 'authenticated' : 'error', ...result });
|
|
701
|
+
} catch (err) {
|
|
702
|
+
return res.status(500).json({ status: 'error', error: err.message });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (method === 'chatgpt-plus') {
|
|
707
|
+
daemon.audit.log('provider.login.started', { provider: id, method: 'chatgpt-plus' });
|
|
708
|
+
return new Promise((resolve) => {
|
|
709
|
+
let responded = false;
|
|
710
|
+
const respond = (data, status) => {
|
|
711
|
+
if (responded) return;
|
|
712
|
+
responded = true;
|
|
713
|
+
clearInstallCache();
|
|
714
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
715
|
+
if (status) res.status(status).json(data);
|
|
716
|
+
else res.json(data);
|
|
717
|
+
resolve();
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const proc = spawn('codex', ['login'], {
|
|
721
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
722
|
+
});
|
|
723
|
+
let stdout = '';
|
|
724
|
+
let stderr = '';
|
|
725
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
726
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
727
|
+
|
|
728
|
+
const timeout = setTimeout(() => {
|
|
729
|
+
const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
|
|
730
|
+
respond(urlMatch
|
|
731
|
+
? { status: 'pending', url: urlMatch[0] }
|
|
732
|
+
: { status: 'pending', message: 'Login started — check your browser' });
|
|
733
|
+
}, 5000);
|
|
734
|
+
|
|
735
|
+
proc.on('close', (code) => {
|
|
736
|
+
clearTimeout(timeout);
|
|
737
|
+
respond(code === 0
|
|
738
|
+
? { status: 'authenticated' }
|
|
739
|
+
: { status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
proc.on('error', (err) => {
|
|
743
|
+
clearTimeout(timeout);
|
|
744
|
+
respond({ status: 'error', error: err.message }, 500);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return res.status(400).json({ error: 'Provide either { key: "..." } or { method: "chatgpt-plus" }' });
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
app.post('/api/providers/:id/set-path', async (req, res) => {
|
|
754
|
+
const { id } = req.params;
|
|
755
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
756
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const { path: customPath } = req.body || {};
|
|
760
|
+
if (!customPath || typeof customPath !== 'string') {
|
|
761
|
+
return res.status(400).json({ error: 'path is required' });
|
|
762
|
+
}
|
|
763
|
+
if (customPath.length > 500) {
|
|
764
|
+
return res.status(400).json({ error: 'Path too long' });
|
|
765
|
+
}
|
|
766
|
+
if (!customPath.startsWith('/')) {
|
|
767
|
+
return res.status(400).json({ error: 'Path must be absolute' });
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (!existsSync(customPath)) {
|
|
771
|
+
return res.status(400).json({ error: `Path does not exist: ${customPath}` });
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
try {
|
|
775
|
+
const stat = statSync(customPath);
|
|
776
|
+
if (!stat.isFile()) {
|
|
777
|
+
return res.status(400).json({ error: 'Path must point to a file, not a directory' });
|
|
778
|
+
}
|
|
779
|
+
const mode = stat.mode;
|
|
780
|
+
const isExecutable = !!(mode & 0o111);
|
|
781
|
+
if (!isExecutable) {
|
|
782
|
+
return res.status(400).json({ error: 'File is not executable' });
|
|
783
|
+
}
|
|
784
|
+
} catch (err) {
|
|
785
|
+
return res.status(400).json({ error: `Cannot stat path: ${err.message}` });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (!daemon.config.providerPaths) daemon.config.providerPaths = {};
|
|
789
|
+
daemon.config.providerPaths[id] = customPath;
|
|
790
|
+
|
|
791
|
+
const { saveConfig } = await import('./firstrun.js');
|
|
792
|
+
saveConfig(daemon.grooveDir, daemon.config);
|
|
793
|
+
|
|
794
|
+
setProviderPaths(daemon.config.providerPaths);
|
|
795
|
+
clearInstallCache();
|
|
796
|
+
|
|
797
|
+
daemon.audit.log('provider.setPath', { provider: id, path: customPath });
|
|
798
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
799
|
+
|
|
800
|
+
res.json({ ok: true, path: customPath });
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
app.post('/api/providers/:id/verify', async (req, res) => {
|
|
804
|
+
const { id } = req.params;
|
|
805
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
806
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
clearInstallCache();
|
|
810
|
+
const providerObj = getProvider(id);
|
|
811
|
+
if (!providerObj) {
|
|
812
|
+
return res.json({ installed: false, authenticated: false, version: null, error: 'Unknown provider' });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const installed = providerObj.constructor.isInstalled();
|
|
816
|
+
let authenticated = false;
|
|
817
|
+
let version = null;
|
|
818
|
+
let error = null;
|
|
819
|
+
|
|
820
|
+
if (installed) {
|
|
821
|
+
const authStatus = providerObj.constructor.isAuthenticated?.();
|
|
822
|
+
authenticated = !!(authStatus?.authenticated);
|
|
823
|
+
|
|
824
|
+
const command = providerObj.constructor.command;
|
|
825
|
+
const customPath = getProviderPath(id);
|
|
826
|
+
const bin = customPath || command;
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
version = execFileSync(bin, ['--version'], {
|
|
830
|
+
encoding: 'utf8',
|
|
831
|
+
timeout: 5000,
|
|
832
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
833
|
+
}).trim();
|
|
834
|
+
} catch (err) {
|
|
835
|
+
version = null;
|
|
836
|
+
error = `Version check failed: ${err.message?.slice(0, 200) || 'unknown error'}`;
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
error = 'Provider not installed';
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
843
|
+
|
|
844
|
+
res.json({ installed, authenticated, version, error });
|
|
845
|
+
});
|
|
846
|
+
|
|
579
847
|
// --- Local Models (GGUF via HuggingFace) ---
|
|
580
848
|
|
|
581
849
|
app.get('/api/models/installed', (req, res) => {
|
|
@@ -3831,9 +4099,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3831
4099
|
const installed = providerObj ? providerObj.constructor.isInstalled() : false;
|
|
3832
4100
|
|
|
3833
4101
|
if (code === 0 && installed) {
|
|
4102
|
+
clearInstallCache();
|
|
3834
4103
|
write({ status: 'complete', output: `${pkg} installed successfully`, progress: 100, installed: true });
|
|
3835
4104
|
daemon.audit.log('onboarding.installProvider', { provider, pkg, success: true });
|
|
3836
4105
|
daemon.broadcast({ type: 'onboarding:provider-installed', provider });
|
|
4106
|
+
daemon.broadcast({ type: 'provider:status-changed', provider });
|
|
3837
4107
|
} else {
|
|
3838
4108
|
const reason = code !== 0
|
|
3839
4109
|
? (errOutput || output).slice(-500)
|
|
@@ -45,6 +45,7 @@ import { ConversationManager } from './conversations.js';
|
|
|
45
45
|
import { Toys } from './toys.js';
|
|
46
46
|
import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
|
|
47
47
|
import { bindDaemon as bindGrooveNetworkDaemon } from './providers/groove-network.js';
|
|
48
|
+
import { setProviderPaths } from './providers/index.js';
|
|
48
49
|
|
|
49
50
|
const DEFAULT_PORT = 31415;
|
|
50
51
|
const DEFAULT_HOST = '127.0.0.1';
|
|
@@ -113,6 +114,8 @@ export class Daemon {
|
|
|
113
114
|
this.config = loadConfig(this.grooveDir);
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
if (this.config.providerPaths) setProviderPaths(this.config.providerPaths);
|
|
118
|
+
|
|
116
119
|
// Initialize core components
|
|
117
120
|
this.state = new StateManager(this.grooveDir);
|
|
118
121
|
this.registry = new Registry(this.state);
|
|
@@ -36,4 +36,12 @@ export class Provider {
|
|
|
36
36
|
streamChat(messages, model, apiKey, onChunk, onDone, onError) {
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
static setupGuide() {
|
|
41
|
+
return { installSteps: [], authMethods: [], authInstructions: {} };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static authMethods() {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
39
47
|
}
|
|
@@ -340,4 +340,56 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
340
340
|
child.unref();
|
|
341
341
|
return { pid: child.pid };
|
|
342
342
|
}
|
|
343
|
+
|
|
344
|
+
static setupGuide() {
|
|
345
|
+
return {
|
|
346
|
+
installSteps: ['Installing Claude Code...', 'This may take a minute'],
|
|
347
|
+
authMethods: ['subscription', 'api-key'],
|
|
348
|
+
authInstructions: {
|
|
349
|
+
subscriptionLoginHelp: 'Sign in with your Anthropic account',
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
static authMethods() {
|
|
355
|
+
return ['subscription', 'api-key'];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
static async startLogin() {
|
|
359
|
+
return new Promise((resolve) => {
|
|
360
|
+
const child = cpSpawn('claude', ['auth', 'login', '--claudeai'], {
|
|
361
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
362
|
+
});
|
|
363
|
+
let stdout = '';
|
|
364
|
+
let stderr = '';
|
|
365
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
366
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
367
|
+
|
|
368
|
+
const timeout = setTimeout(() => {
|
|
369
|
+
const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
|
|
370
|
+
if (urlMatch) {
|
|
371
|
+
resolve({ status: 'pending', url: urlMatch[0], pid: child.pid });
|
|
372
|
+
} else {
|
|
373
|
+
resolve({ status: 'pending', message: 'Login started — check your browser', pid: child.pid });
|
|
374
|
+
}
|
|
375
|
+
}, 3000);
|
|
376
|
+
|
|
377
|
+
child.on('close', (code) => {
|
|
378
|
+
clearTimeout(timeout);
|
|
379
|
+
if (code === 0) {
|
|
380
|
+
resolve({ status: 'authenticated' });
|
|
381
|
+
} else {
|
|
382
|
+
const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
|
|
383
|
+
resolve(urlMatch
|
|
384
|
+
? { status: 'pending', url: urlMatch[0], pid: child.pid }
|
|
385
|
+
: { status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
child.on('error', (err) => {
|
|
390
|
+
clearTimeout(timeout);
|
|
391
|
+
resolve({ status: 'error', error: err.message });
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
343
395
|
}
|
|
@@ -323,4 +323,19 @@ export class CodexProvider extends Provider {
|
|
|
323
323
|
return null;
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
|
+
|
|
327
|
+
static setupGuide() {
|
|
328
|
+
return {
|
|
329
|
+
installSteps: ['Installing Codex CLI...', 'This may take a minute'],
|
|
330
|
+
authMethods: ['api-key', 'chatgpt-plus'],
|
|
331
|
+
authInstructions: {
|
|
332
|
+
apiKeyHelp: 'Get your API key from platform.openai.com/api-keys',
|
|
333
|
+
chatgptPlusHelp: 'Sign in with your ChatGPT Plus account',
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
static authMethods() {
|
|
339
|
+
return ['api-key', 'chatgpt-plus'];
|
|
340
|
+
}
|
|
326
341
|
}
|
|
@@ -219,4 +219,18 @@ export class GeminiProvider extends Provider {
|
|
|
219
219
|
return null;
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
static setupGuide() {
|
|
224
|
+
return {
|
|
225
|
+
installSteps: ['Installing Gemini CLI...', 'This may take a minute'],
|
|
226
|
+
authMethods: ['api-key'],
|
|
227
|
+
authInstructions: {
|
|
228
|
+
keyInstructions: 'Get your API key from aistudio.google.com',
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static authMethods() {
|
|
234
|
+
return ['api-key'];
|
|
235
|
+
}
|
|
222
236
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
+
import { dirname as pathDirname } from 'path';
|
|
5
6
|
import { ClaudeCodeProvider } from './claude-code.js';
|
|
6
7
|
import { CodexProvider } from './codex.js';
|
|
7
8
|
import { GeminiProvider } from './gemini.js';
|
|
@@ -11,6 +12,30 @@ import { GrooveNetworkProvider } from './groove-network.js';
|
|
|
11
12
|
|
|
12
13
|
// Electron forks may not inherit the full shell PATH, causing `which` to miss
|
|
13
14
|
// globally-installed CLI tools. Augment PATH with common npm global bin dirs.
|
|
15
|
+
// Custom provider paths from config are prepended when setProviderPaths() is called.
|
|
16
|
+
let _providerPaths = {};
|
|
17
|
+
|
|
18
|
+
export function setProviderPaths(paths) {
|
|
19
|
+
_providerPaths = paths || {};
|
|
20
|
+
_augmentPathWithCustomPaths();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _augmentPathWithCustomPaths() {
|
|
24
|
+
const cur = process.env.PATH || '';
|
|
25
|
+
const dirs = [];
|
|
26
|
+
for (const p of Object.values(_providerPaths)) {
|
|
27
|
+
if (p && typeof p === 'string') {
|
|
28
|
+
const dir = pathDirname(p);
|
|
29
|
+
if (dir && !cur.split(':').includes(dir)) dirs.push(dir);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (dirs.length) process.env.PATH = [...dirs, cur].join(':');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getProviderPath(id) {
|
|
36
|
+
return (_providerPaths && _providerPaths[id]) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
(function augmentPath() {
|
|
15
40
|
const extra = ['/usr/local/bin', '/opt/homebrew/bin'];
|
|
16
41
|
try {
|
|
@@ -78,3 +103,14 @@ export function listProviders() {
|
|
|
78
103
|
export function getInstalledProviders() {
|
|
79
104
|
return listProviders().filter((p) => p.installed);
|
|
80
105
|
}
|
|
106
|
+
|
|
107
|
+
export function getProviderMetadata(id) {
|
|
108
|
+
const p = providers[id];
|
|
109
|
+
if (!p) return null;
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
setupGuide: p.constructor.setupGuide?.() || { installSteps: [], authMethods: [], authInstructions: {} },
|
|
113
|
+
authMethods: p.constructor.authMethods?.() || [],
|
|
114
|
+
installCommand: p.constructor.installCommand(),
|
|
115
|
+
};
|
|
116
|
+
}
|