groove-dev 0.27.70 → 0.27.72
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/CLAUDE.md +0 -7
- 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 +299 -21
- 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 +54 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +16 -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-D5BpdcWS.js → index-CHSXqfwy.js} +1736 -1735
- 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/editor/file-tree.jsx +11 -0
- package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +112 -4
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +10 -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 +299 -21
- 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 +54 -0
- package/packages/daemon/src/providers/codex.js +16 -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-D5BpdcWS.js → index-CHSXqfwy.js} +1736 -1735
- 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/editor/file-tree.jsx +11 -0
- package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +480 -0
- package/packages/gui/src/stores/groove.js +112 -4
- package/packages/gui/src/views/editor.jsx +10 -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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
4
|
import express from 'express';
|
|
5
|
-
import { resolve, dirname, join, sep, relative } from 'path';
|
|
5
|
+
import { resolve, dirname, join, sep, relative, isAbsolute } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, copyFileSync, realpathSync } from 'fs';
|
|
8
8
|
import { spawn, execFile, execFileSync } from 'child_process';
|
|
@@ -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,271 @@ 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
|
+
shell: true,
|
|
615
|
+
env: { ...process.env, NODE_ENV: undefined },
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
let output = '';
|
|
619
|
+
let errOutput = '';
|
|
620
|
+
|
|
621
|
+
proc.stdout.on('data', (data) => {
|
|
622
|
+
output += data.toString();
|
|
623
|
+
write({ status: 'installing', output: data.toString().trim(), progress: 50 });
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
proc.stderr.on('data', (data) => {
|
|
627
|
+
errOutput += data.toString();
|
|
628
|
+
const line = data.toString().trim();
|
|
629
|
+
if (line) write({ status: 'installing', output: line, progress: 50 });
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
proc.on('close', (code) => {
|
|
633
|
+
clearInstallCache();
|
|
634
|
+
const providerObj = getProvider(id);
|
|
635
|
+
const installed = providerObj ? providerObj.constructor.isInstalled() : false;
|
|
636
|
+
|
|
637
|
+
if (code === 0 && installed) {
|
|
638
|
+
write({ status: 'complete', output: `${pkg} installed successfully`, progress: 100, installed: true });
|
|
639
|
+
daemon.audit.log('provider.install', { provider: id, pkg, success: true });
|
|
640
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
641
|
+
} else {
|
|
642
|
+
const reason = code !== 0
|
|
643
|
+
? (errOutput || output).slice(-500)
|
|
644
|
+
: 'Install succeeded but provider binary not found in PATH';
|
|
645
|
+
write({ status: 'error', output: reason, progress: 100, installed: false });
|
|
646
|
+
daemon.audit.log('provider.install', { provider: id, pkg, success: false, code });
|
|
647
|
+
}
|
|
648
|
+
res.end();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
proc.on('error', (err) => {
|
|
652
|
+
write({ status: 'error', output: `Failed to start npm: ${err.message}`, progress: 100, installed: false });
|
|
653
|
+
res.end();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
req.on('close', () => {
|
|
657
|
+
try { proc.kill(); } catch { /* already exited */ }
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
app.post('/api/providers/:id/login', async (req, res) => {
|
|
662
|
+
const { id } = req.params;
|
|
663
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
664
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (id === 'gemini') {
|
|
668
|
+
return res.json({ status: 'not-supported', message: 'Gemini uses API key authentication. Set your key in Settings.' });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (id === 'claude-code') {
|
|
672
|
+
const providerObj = getProvider(id);
|
|
673
|
+
if (!providerObj || !providerObj.constructor.isInstalled()) {
|
|
674
|
+
return res.status(400).json({ error: 'Claude Code is not installed. Install it first.' });
|
|
675
|
+
}
|
|
676
|
+
daemon.audit.log('provider.login.started', { provider: id });
|
|
677
|
+
try {
|
|
678
|
+
const result = await ClaudeCodeProvider.startLogin();
|
|
679
|
+
clearInstallCache();
|
|
680
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
681
|
+
return res.json(result);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
return res.status(500).json({ status: 'error', error: err.message });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (id === 'codex') {
|
|
688
|
+
const providerObj = getProvider(id);
|
|
689
|
+
if (!providerObj || !providerObj.constructor.isInstalled()) {
|
|
690
|
+
return res.status(400).json({ error: 'Codex is not installed. Install it first.' });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const { method, key } = req.body || {};
|
|
694
|
+
|
|
695
|
+
if (key) {
|
|
696
|
+
daemon.audit.log('provider.login.started', { provider: id, method: 'api-key' });
|
|
697
|
+
try {
|
|
698
|
+
const result = await providerObj.constructor.onKeySet(key);
|
|
699
|
+
clearInstallCache();
|
|
700
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
701
|
+
return res.json({ status: result.ok ? 'authenticated' : 'error', ...result });
|
|
702
|
+
} catch (err) {
|
|
703
|
+
return res.status(500).json({ status: 'error', error: err.message });
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (method === 'chatgpt-plus') {
|
|
708
|
+
daemon.audit.log('provider.login.started', { provider: id, method: 'chatgpt-plus' });
|
|
709
|
+
return new Promise((resolve) => {
|
|
710
|
+
let responded = false;
|
|
711
|
+
const respond = (data, status) => {
|
|
712
|
+
if (responded) return;
|
|
713
|
+
responded = true;
|
|
714
|
+
clearInstallCache();
|
|
715
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
716
|
+
if (status) res.status(status).json(data);
|
|
717
|
+
else res.json(data);
|
|
718
|
+
resolve();
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const proc = spawn('codex', ['login'], {
|
|
722
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
723
|
+
shell: true,
|
|
724
|
+
});
|
|
725
|
+
let stdout = '';
|
|
726
|
+
let stderr = '';
|
|
727
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
728
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
729
|
+
|
|
730
|
+
const timeout = setTimeout(() => {
|
|
731
|
+
const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
|
|
732
|
+
respond(urlMatch
|
|
733
|
+
? { status: 'pending', url: urlMatch[0] }
|
|
734
|
+
: { status: 'pending', message: 'Login started — check your browser' });
|
|
735
|
+
}, 5000);
|
|
736
|
+
|
|
737
|
+
proc.on('close', (code) => {
|
|
738
|
+
clearTimeout(timeout);
|
|
739
|
+
respond(code === 0
|
|
740
|
+
? { status: 'authenticated' }
|
|
741
|
+
: { status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
proc.on('error', (err) => {
|
|
745
|
+
clearTimeout(timeout);
|
|
746
|
+
respond({ status: 'error', error: err.message }, 500);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return res.status(400).json({ error: 'Provide either { key: "..." } or { method: "chatgpt-plus" }' });
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
app.post('/api/providers/:id/set-path', async (req, res) => {
|
|
756
|
+
const { id } = req.params;
|
|
757
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
758
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const { path: customPath } = req.body || {};
|
|
762
|
+
if (!customPath || typeof customPath !== 'string') {
|
|
763
|
+
return res.status(400).json({ error: 'path is required' });
|
|
764
|
+
}
|
|
765
|
+
if (customPath.length > 500) {
|
|
766
|
+
return res.status(400).json({ error: 'Path too long' });
|
|
767
|
+
}
|
|
768
|
+
if (!isAbsolute(customPath)) {
|
|
769
|
+
return res.status(400).json({ error: 'Path must be absolute' });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (!existsSync(customPath)) {
|
|
773
|
+
return res.status(400).json({ error: `Path does not exist: ${customPath}` });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
const stat = statSync(customPath);
|
|
778
|
+
if (!stat.isFile()) {
|
|
779
|
+
return res.status(400).json({ error: 'Path must point to a file, not a directory' });
|
|
780
|
+
}
|
|
781
|
+
const mode = stat.mode;
|
|
782
|
+
const isExecutable = !!(mode & 0o111);
|
|
783
|
+
if (!isExecutable) {
|
|
784
|
+
return res.status(400).json({ error: 'File is not executable' });
|
|
785
|
+
}
|
|
786
|
+
} catch (err) {
|
|
787
|
+
return res.status(400).json({ error: `Cannot stat path: ${err.message}` });
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (!daemon.config.providerPaths) daemon.config.providerPaths = {};
|
|
791
|
+
daemon.config.providerPaths[id] = customPath;
|
|
792
|
+
|
|
793
|
+
const { saveConfig } = await import('./firstrun.js');
|
|
794
|
+
saveConfig(daemon.grooveDir, daemon.config);
|
|
795
|
+
|
|
796
|
+
setProviderPaths(daemon.config.providerPaths);
|
|
797
|
+
clearInstallCache();
|
|
798
|
+
|
|
799
|
+
daemon.audit.log('provider.setPath', { provider: id, path: customPath });
|
|
800
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
801
|
+
|
|
802
|
+
res.json({ ok: true, path: customPath });
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
app.post('/api/providers/:id/verify', async (req, res) => {
|
|
806
|
+
const { id } = req.params;
|
|
807
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
808
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
clearInstallCache();
|
|
812
|
+
const providerObj = getProvider(id);
|
|
813
|
+
if (!providerObj) {
|
|
814
|
+
return res.json({ installed: false, authenticated: false, version: null, error: 'Unknown provider' });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const installed = providerObj.constructor.isInstalled();
|
|
818
|
+
let authenticated = false;
|
|
819
|
+
let version = null;
|
|
820
|
+
let error = null;
|
|
821
|
+
|
|
822
|
+
if (installed) {
|
|
823
|
+
const authStatus = providerObj.constructor.isAuthenticated?.();
|
|
824
|
+
authenticated = !!(authStatus?.authenticated);
|
|
825
|
+
|
|
826
|
+
const command = providerObj.constructor.command;
|
|
827
|
+
const customPath = getProviderPath(id);
|
|
828
|
+
const bin = customPath || command;
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
version = execFileSync(bin, ['--version'], {
|
|
832
|
+
encoding: 'utf8',
|
|
833
|
+
timeout: 5000,
|
|
834
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
835
|
+
shell: true,
|
|
836
|
+
}).trim();
|
|
837
|
+
} catch (err) {
|
|
838
|
+
version = null;
|
|
839
|
+
error = `Version check failed: ${err.message?.slice(0, 200) || 'unknown error'}`;
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
error = 'Provider not installed';
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
846
|
+
|
|
847
|
+
res.json({ installed, authenticated, version, error });
|
|
848
|
+
});
|
|
849
|
+
|
|
579
850
|
// --- Local Models (GGUF via HuggingFace) ---
|
|
580
851
|
|
|
581
852
|
app.get('/api/models/installed', (req, res) => {
|
|
@@ -765,7 +1036,7 @@ export function createApi(app, daemon) {
|
|
|
765
1036
|
}
|
|
766
1037
|
try {
|
|
767
1038
|
daemon.setProjectDir(dirPath);
|
|
768
|
-
|
|
1039
|
+
editorRootOverride = null;
|
|
769
1040
|
res.json({ projectDir: daemon.projectDir, recentProjects: daemon.config.recentProjects || [] });
|
|
770
1041
|
} catch (err) {
|
|
771
1042
|
res.status(400).json({ error: err.message });
|
|
@@ -2392,12 +2663,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2392
2663
|
return LANG_MAP[ext] || 'text';
|
|
2393
2664
|
}
|
|
2394
2665
|
|
|
2395
|
-
const IGNORED_NAMES = new Set(['.
|
|
2666
|
+
const IGNORED_NAMES = new Set(['.DS_Store', '__pycache__']);
|
|
2396
2667
|
|
|
2397
|
-
// Editor root directory —
|
|
2398
|
-
|
|
2668
|
+
// Editor root directory — always tracks daemon.projectDir unless explicitly
|
|
2669
|
+
// overridden via POST /api/files/root. Reset on project-dir change.
|
|
2670
|
+
let editorRootOverride = null;
|
|
2399
2671
|
|
|
2400
|
-
function getEditorRoot() { return
|
|
2672
|
+
function getEditorRoot() { return editorRootOverride || daemon.projectDir; }
|
|
2401
2673
|
|
|
2402
2674
|
function validateFilePath(relPath, projectDir) {
|
|
2403
2675
|
if (!relPath || typeof relPath !== 'string') return { error: 'path is required' };
|
|
@@ -2421,13 +2693,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2421
2693
|
|
|
2422
2694
|
// Get/set the editor working directory
|
|
2423
2695
|
app.get('/api/files/root', (req, res) => {
|
|
2424
|
-
res.json({ root:
|
|
2696
|
+
res.json({ root: getEditorRoot() });
|
|
2425
2697
|
});
|
|
2426
2698
|
|
|
2427
2699
|
app.post('/api/files/root', (req, res) => {
|
|
2428
2700
|
const { root } = req.body || {};
|
|
2429
2701
|
if (!root || typeof root !== 'string') return res.status(400).json({ error: 'root path is required' });
|
|
2430
|
-
// Must be absolute and exist
|
|
2431
2702
|
if (!root.startsWith('/')) return res.status(400).json({ error: 'root must be an absolute path' });
|
|
2432
2703
|
if (root.includes('\0') || root.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
2433
2704
|
if (!existsSync(root)) return res.status(404).json({ error: 'Directory not found' });
|
|
@@ -2435,9 +2706,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2435
2706
|
const stat = statSync(root);
|
|
2436
2707
|
if (!stat.isDirectory()) return res.status(400).json({ error: 'Path is not a directory' });
|
|
2437
2708
|
} catch { return res.status(400).json({ error: 'Cannot access directory' }); }
|
|
2438
|
-
|
|
2709
|
+
editorRootOverride = root;
|
|
2439
2710
|
daemon.audit.log('editor.root.set', { root });
|
|
2440
|
-
res.json({ ok: true, root:
|
|
2711
|
+
res.json({ ok: true, root: getEditorRoot() });
|
|
2441
2712
|
});
|
|
2442
2713
|
|
|
2443
2714
|
// File tree — returns dirs + files for a given path
|
|
@@ -2462,18 +2733,22 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2462
2733
|
const raw = readdirSync(fullPath, { withFileTypes: true });
|
|
2463
2734
|
const entries = [];
|
|
2464
2735
|
|
|
2465
|
-
|
|
2466
|
-
|
|
2736
|
+
const dirs = raw.filter((e) => {
|
|
2737
|
+
if (e.name === '.DS_Store') return false;
|
|
2467
2738
|
if (e.isDirectory()) return true;
|
|
2468
|
-
if (e.isSymbolicLink()) {
|
|
2739
|
+
if (e.isSymbolicLink()) {
|
|
2740
|
+
try { return statSync(resolve(fullPath, e.name)).isDirectory(); }
|
|
2741
|
+
catch { return true; }
|
|
2742
|
+
}
|
|
2469
2743
|
return false;
|
|
2470
|
-
};
|
|
2471
|
-
const dirs = raw.filter((e) => isDir(e) && !IGNORED_NAMES.has(e.name) && !e.name.startsWith('.'))
|
|
2472
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2744
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2473
2745
|
const files = raw.filter((e) => {
|
|
2474
|
-
if (e.name
|
|
2746
|
+
if (e.name === '.DS_Store') return false;
|
|
2475
2747
|
if (e.isFile()) return true;
|
|
2476
|
-
if (e.isSymbolicLink()) {
|
|
2748
|
+
if (e.isSymbolicLink()) {
|
|
2749
|
+
try { return statSync(resolve(fullPath, e.name)).isFile(); }
|
|
2750
|
+
catch { return false; }
|
|
2751
|
+
}
|
|
2477
2752
|
return false;
|
|
2478
2753
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2479
2754
|
|
|
@@ -2483,7 +2758,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2483
2758
|
let hasChildren = false;
|
|
2484
2759
|
try {
|
|
2485
2760
|
const children = readdirSync(childFull, { withFileTypes: true });
|
|
2486
|
-
hasChildren = children.some((c) =>
|
|
2761
|
+
hasChildren = children.some((c) => c.name !== '.DS_Store');
|
|
2487
2762
|
} catch { /* unreadable */ }
|
|
2488
2763
|
entries.push({ name: d.name, type: 'dir', path: childPath, hasChildren });
|
|
2489
2764
|
}
|
|
@@ -3809,6 +4084,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3809
4084
|
|
|
3810
4085
|
const proc = spawn('npm', ['install', '-g', pkg], {
|
|
3811
4086
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4087
|
+
shell: true,
|
|
3812
4088
|
env: { ...process.env, NODE_ENV: undefined },
|
|
3813
4089
|
});
|
|
3814
4090
|
|
|
@@ -3831,9 +4107,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3831
4107
|
const installed = providerObj ? providerObj.constructor.isInstalled() : false;
|
|
3832
4108
|
|
|
3833
4109
|
if (code === 0 && installed) {
|
|
4110
|
+
clearInstallCache();
|
|
3834
4111
|
write({ status: 'complete', output: `${pkg} installed successfully`, progress: 100, installed: true });
|
|
3835
4112
|
daemon.audit.log('onboarding.installProvider', { provider, pkg, success: true });
|
|
3836
4113
|
daemon.broadcast({ type: 'onboarding:provider-installed', provider });
|
|
4114
|
+
daemon.broadcast({ type: 'provider:status-changed', provider });
|
|
3837
4115
|
} else {
|
|
3838
4116
|
const reason = code !== 0
|
|
3839
4117
|
? (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
|
}
|
|
@@ -336,8 +336,62 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
336
336
|
const child = cpSpawn('claude', ['auth', 'login', '--claudeai'], {
|
|
337
337
|
detached: true,
|
|
338
338
|
stdio: 'ignore',
|
|
339
|
+
shell: true,
|
|
339
340
|
});
|
|
340
341
|
child.unref();
|
|
341
342
|
return { pid: child.pid };
|
|
342
343
|
}
|
|
344
|
+
|
|
345
|
+
static setupGuide() {
|
|
346
|
+
return {
|
|
347
|
+
installSteps: ['Installing Claude Code...', 'This may take a minute'],
|
|
348
|
+
authMethods: ['subscription', 'api-key'],
|
|
349
|
+
authInstructions: {
|
|
350
|
+
subscriptionLoginHelp: 'Sign in with your Anthropic account',
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
static authMethods() {
|
|
356
|
+
return ['subscription', 'api-key'];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
static async startLogin() {
|
|
360
|
+
return new Promise((resolve) => {
|
|
361
|
+
const child = cpSpawn('claude', ['auth', 'login', '--claudeai'], {
|
|
362
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
363
|
+
shell: true,
|
|
364
|
+
});
|
|
365
|
+
let stdout = '';
|
|
366
|
+
let stderr = '';
|
|
367
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
368
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
369
|
+
|
|
370
|
+
const timeout = setTimeout(() => {
|
|
371
|
+
const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
|
|
372
|
+
if (urlMatch) {
|
|
373
|
+
resolve({ status: 'pending', url: urlMatch[0], pid: child.pid });
|
|
374
|
+
} else {
|
|
375
|
+
resolve({ status: 'pending', message: 'Login started — check your browser', pid: child.pid });
|
|
376
|
+
}
|
|
377
|
+
}, 3000);
|
|
378
|
+
|
|
379
|
+
child.on('close', (code) => {
|
|
380
|
+
clearTimeout(timeout);
|
|
381
|
+
if (code === 0) {
|
|
382
|
+
resolve({ status: 'authenticated' });
|
|
383
|
+
} else {
|
|
384
|
+
const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
|
|
385
|
+
resolve(urlMatch
|
|
386
|
+
? { status: 'pending', url: urlMatch[0], pid: child.pid }
|
|
387
|
+
: { status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
child.on('error', (err) => {
|
|
392
|
+
clearTimeout(timeout);
|
|
393
|
+
resolve({ status: 'error', error: err.message });
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
343
397
|
}
|
|
@@ -72,6 +72,7 @@ export class CodexProvider extends Provider {
|
|
|
72
72
|
return new Promise((res) => {
|
|
73
73
|
const proc = spawn('codex', ['login', '--with-api-key'], {
|
|
74
74
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
75
|
+
shell: true,
|
|
75
76
|
timeout: 15000,
|
|
76
77
|
});
|
|
77
78
|
let stderr = '';
|
|
@@ -323,4 +324,19 @@ export class CodexProvider extends Provider {
|
|
|
323
324
|
return null;
|
|
324
325
|
}
|
|
325
326
|
}
|
|
327
|
+
|
|
328
|
+
static setupGuide() {
|
|
329
|
+
return {
|
|
330
|
+
installSteps: ['Installing Codex CLI...', 'This may take a minute'],
|
|
331
|
+
authMethods: ['api-key', 'chatgpt-plus'],
|
|
332
|
+
authInstructions: {
|
|
333
|
+
apiKeyHelp: 'Get your API key from platform.openai.com/api-keys',
|
|
334
|
+
chatgptPlusHelp: 'Sign in with your ChatGPT Plus account',
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
static authMethods() {
|
|
340
|
+
return ['api-key', 'chatgpt-plus'];
|
|
341
|
+
}
|
|
326
342
|
}
|
|
@@ -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, delimiter as pathDelimiter } 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(pathDelimiter).includes(dir)) dirs.push(dir);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (dirs.length) process.env.PATH = [...dirs, cur].join(pathDelimiter);
|
|
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
|
+
}
|