groove-dev 0.27.66 → 0.27.67

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.
Files changed (49) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +12 -31
  4. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +25 -2
  5. package/node_modules/@groove-dev/gui/dist/assets/index-MPNqazCA.js +8614 -0
  6. package/node_modules/@groove-dev/gui/dist/assets/index-YeunozTU.css +1 -0
  7. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  8. package/node_modules/@groove-dev/gui/package.json +1 -1
  9. package/node_modules/@groove-dev/gui/src/app.jsx +5 -1
  10. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +3 -1
  11. package/node_modules/@groove-dev/gui/src/components/agents/folder-browser.jsx +5 -4
  12. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +1 -2
  13. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -9
  14. package/node_modules/@groove-dev/gui/src/components/layout/project-picker.jsx +3 -1
  15. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +132 -0
  16. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +3 -1
  17. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +1 -2
  18. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +2 -0
  19. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +3 -0
  20. package/node_modules/@groove-dev/gui/src/stores/groove.js +10 -2
  21. package/node_modules/@groove-dev/gui/src/views/agents.jsx +14 -1
  22. package/node_modules/@groove-dev/gui/src/views/settings.jsx +3 -1
  23. package/package.json +1 -1
  24. package/packages/cli/package.json +1 -1
  25. package/packages/daemon/package.json +1 -1
  26. package/packages/daemon/src/api.js +12 -31
  27. package/packages/daemon/src/tunnel-manager.js +25 -2
  28. package/packages/gui/dist/assets/index-MPNqazCA.js +8614 -0
  29. package/packages/gui/dist/assets/index-YeunozTU.css +1 -0
  30. package/packages/gui/dist/index.html +2 -2
  31. package/packages/gui/package.json +1 -1
  32. package/packages/gui/src/app.jsx +5 -1
  33. package/packages/gui/src/components/agents/agent-config.jsx +3 -1
  34. package/packages/gui/src/components/agents/folder-browser.jsx +5 -4
  35. package/packages/gui/src/components/layout/app-shell.jsx +1 -2
  36. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -9
  37. package/packages/gui/src/components/layout/project-picker.jsx +3 -1
  38. package/packages/gui/src/components/layout/welcome-splash.jsx +132 -0
  39. package/packages/gui/src/components/onboarding/setup-wizard.jsx +3 -1
  40. package/packages/gui/src/components/pro/upgrade-modal.jsx +1 -2
  41. package/packages/gui/src/components/settings/server-dialog.jsx +2 -0
  42. package/packages/gui/src/components/settings/ssh-wizard.jsx +3 -0
  43. package/packages/gui/src/stores/groove.js +10 -2
  44. package/packages/gui/src/views/agents.jsx +14 -1
  45. package/packages/gui/src/views/settings.jsx +3 -1
  46. package/node_modules/@groove-dev/gui/dist/assets/index-BvvSZvQz.js +0 -8614
  47. package/node_modules/@groove-dev/gui/dist/assets/index-DFp5IOnd.css +0 -1
  48. package/packages/gui/dist/assets/index-BvvSZvQz.js +0 -8614
  49. package/packages/gui/dist/assets/index-DFp5IOnd.css +0 -1
@@ -34,6 +34,7 @@ export const useGrooveStore = create((set, get) => ({
34
34
  ws: null,
35
35
  daemonHost: null,
36
36
  tunneled: false,
37
+ remoteHomedir: null,
37
38
 
38
39
  // ── Teams ─────────────────────────────────────────────────
39
40
  teams: [],
@@ -172,6 +173,7 @@ export const useGrooveStore = create((set, get) => ({
172
173
  const isTunneled = String(s.port) !== browserPort;
173
174
  if (isTunneled) updates.tunneled = true;
174
175
  if (s.version) updates.version = s.version;
176
+ if (s.homedir) updates.remoteHomedir = s.homedir;
175
177
  if (Object.keys(updates).length > 0) set(updates);
176
178
  if (isTunneled) get().fetchProjectDir();
177
179
  }).catch(() => {});
@@ -907,7 +909,7 @@ export const useGrooveStore = create((set, get) => ({
907
909
  clearInterval(plannerPollInterval);
908
910
  plannerPollInterval = null;
909
911
  }
910
- set({ connected: false, hydrated: false, ws: null, daemonHost: null, tunneled: false });
912
+ set({ connected: false, hydrated: false, ws: null, daemonHost: null, tunneled: false, remoteHomedir: null });
911
913
  setTimeout(() => get().connect(), 2000);
912
914
  };
913
915
  ws.onerror = () => ws.close();
@@ -1540,7 +1542,13 @@ export const useGrooveStore = create((set, get) => ({
1540
1542
  get().addToast('success', `Spawned ${agent.name}`);
1541
1543
  return agent;
1542
1544
  } catch (err) {
1543
- get().addToast('error', 'Spawn failed', err.message);
1545
+ let detail = err.message;
1546
+ if (detail?.includes('workingDir must be within project directory')) {
1547
+ const projDir = get().projectDir || 'unknown';
1548
+ const workDir = config.workingDir || 'default';
1549
+ detail = `workingDir "${workDir}" is outside project directory "${projDir}". Change the project directory or pick a subfolder within it.`;
1550
+ }
1551
+ get().addToast('error', 'Spawn failed', detail);
1544
1552
  throw err;
1545
1553
  }
1546
1554
  },
@@ -10,7 +10,7 @@ import { RootNode } from '../components/agents/root-node';
10
10
  import { cn } from '../lib/cn';
11
11
  import { Button } from '../components/ui/button';
12
12
  import { Badge } from '../components/ui/badge';
13
- import { Plus, Users, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen } from 'lucide-react';
13
+ import { Plus, Users, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Radio } from 'lucide-react';
14
14
  import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../components/ui/context-menu';
15
15
 
16
16
  const NODE_TYPES = { agentNode: AgentNode, rootNode: RootNode };
@@ -629,6 +629,19 @@ function EmptyState({ onPlanner, onSpawn }) {
629
629
  <div className="text-xs text-text-3 font-sans mt-0.5">Choose a role and configure</div>
630
630
  </div>
631
631
  </button>
632
+
633
+ <button
634
+ onClick={() => useGrooveStore.getState().toggleQuickConnect()}
635
+ className="w-full flex items-center gap-3 p-4 rounded-lg border border-border bg-surface-1 hover:bg-surface-2 hover:border-border transition-all cursor-pointer group text-left"
636
+ >
637
+ <div className="w-10 h-10 rounded-lg bg-surface-4 flex items-center justify-center group-hover:scale-110 transition-transform flex-shrink-0">
638
+ <Radio size={20} className="text-text-1" />
639
+ </div>
640
+ <div className="min-w-0">
641
+ <div className="text-sm font-semibold text-text-0 font-sans">Connect to Remote Server</div>
642
+ <div className="text-xs text-text-3 font-sans mt-0.5">SSH tunnel to a remote machine</div>
643
+ </div>
644
+ </button>
632
645
  </div>
633
646
 
634
647
  {window.groove?.openFolder && (
@@ -1035,6 +1035,7 @@ export default function SettingsView() {
1035
1035
  const [loading, setLoading] = useState(true);
1036
1036
  const [folderBrowserOpen, setFolderBrowserOpen] = useState(false);
1037
1037
  const addToast = useGrooveStore((s) => s.addToast);
1038
+ const remoteHomedir = useGrooveStore((s) => s.remoteHomedir);
1038
1039
 
1039
1040
  function loadProviders() {
1040
1041
  api.get('/providers').then((d) => setProviders(Array.isArray(d) ? d : [])).catch(() => {});
@@ -1296,7 +1297,8 @@ export default function SettingsView() {
1296
1297
  <FolderBrowser
1297
1298
  open={folderBrowserOpen}
1298
1299
  onOpenChange={setFolderBrowserOpen}
1299
- currentPath={config?.defaultWorkingDir || '/'}
1300
+ currentPath={config?.defaultWorkingDir || remoteHomedir || '/'}
1301
+ homePath={remoteHomedir}
1300
1302
  onSelect={(dir) => updateConfig('defaultWorkingDir', dir)}
1301
1303
  />
1302
1304
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.66",
3
+ "version": "0.27.67",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.66",
3
+ "version": "0.27.67",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.66",
3
+ "version": "0.27.67",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -744,6 +744,7 @@ export function createApi(app, daemon) {
744
744
  port: daemon.port,
745
745
  projectDir: daemon.projectDir,
746
746
  edition: sub.active ? 'pro' : 'community',
747
+ homedir: homedir(),
747
748
  });
748
749
  });
749
750
 
@@ -763,6 +764,7 @@ export function createApi(app, daemon) {
763
764
  }
764
765
  try {
765
766
  daemon.setProjectDir(dirPath);
767
+ editorRootDir = daemon.projectDir;
766
768
  res.json({ projectDir: daemon.projectDir, recentProjects: daemon.config.recentProjects || [] });
767
769
  } catch (err) {
768
770
  res.status(400).json({ error: err.message });
@@ -3635,11 +3637,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
3635
3637
 
3636
3638
  // --- Tunnels (Remote Access) ---
3637
3639
 
3638
- app.get('/api/tunnels', proOnly, (req, res) => {
3640
+ app.get('/api/tunnels', (req, res) => {
3639
3641
  res.json(daemon.tunnelManager.getSaved());
3640
3642
  });
3641
3643
 
3642
- app.post('/api/tunnels', proOnly, (req, res) => {
3644
+ app.post('/api/tunnels', (req, res) => {
3643
3645
  try {
3644
3646
  const { name, host, user, port, sshKeyPath, autoStart, autoConnect } = req.body;
3645
3647
  if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name is required (string)' });
@@ -3651,7 +3653,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3651
3653
  }
3652
3654
  });
3653
3655
 
3654
- app.patch('/api/tunnels/:id', proOnly, (req, res) => {
3656
+ app.patch('/api/tunnels/:id', (req, res) => {
3655
3657
  try {
3656
3658
  const result = daemon.tunnelManager.update(req.params.id, req.body);
3657
3659
  res.json(result);
@@ -3660,7 +3662,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3660
3662
  }
3661
3663
  });
3662
3664
 
3663
- app.delete('/api/tunnels/:id', proOnly, async (req, res) => {
3665
+ app.delete('/api/tunnels/:id', async (req, res) => {
3664
3666
  try {
3665
3667
  await daemon.tunnelManager.delete(req.params.id);
3666
3668
  res.json({ ok: true });
@@ -3669,7 +3671,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3669
3671
  }
3670
3672
  });
3671
3673
 
3672
- app.post('/api/tunnels/:id/test', proOnly, async (req, res) => {
3674
+ app.post('/api/tunnels/:id/test', async (req, res) => {
3673
3675
  try {
3674
3676
  const result = await daemon.tunnelManager.test(req.params.id);
3675
3677
  res.json(result);
@@ -3678,7 +3680,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3678
3680
  }
3679
3681
  });
3680
3682
 
3681
- app.post('/api/tunnels/:id/connect', proOnly, async (req, res) => {
3683
+ app.post('/api/tunnels/:id/connect', async (req, res) => {
3682
3684
  try {
3683
3685
  const opts = {};
3684
3686
  if (req.body?.skipTest && req.body?.testResult) {
@@ -3694,7 +3696,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3694
3696
  }
3695
3697
  });
3696
3698
 
3697
- app.post('/api/tunnels/:id/disconnect', proOnly, async (req, res) => {
3699
+ app.post('/api/tunnels/:id/disconnect', async (req, res) => {
3698
3700
  try {
3699
3701
  await daemon.tunnelManager.disconnect(req.params.id);
3700
3702
  res.json({ ok: true });
@@ -3703,7 +3705,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3703
3705
  }
3704
3706
  });
3705
3707
 
3706
- app.post('/api/tunnels/:id/install', proOnly, async (req, res) => {
3708
+ app.post('/api/tunnels/:id/install', async (req, res) => {
3707
3709
  try {
3708
3710
  const result = await daemon.tunnelManager.remoteInstall(req.params.id);
3709
3711
  res.json(result);
@@ -3712,7 +3714,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3712
3714
  }
3713
3715
  });
3714
3716
 
3715
- app.post('/api/tunnels/:id/start', proOnly, async (req, res) => {
3717
+ app.post('/api/tunnels/:id/start', async (req, res) => {
3716
3718
  try {
3717
3719
  await daemon.tunnelManager.autoStart(req.params.id);
3718
3720
  res.json({ ok: true });
@@ -3721,7 +3723,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3721
3723
  }
3722
3724
  });
3723
3725
 
3724
- app.get('/api/tunnels/:id/status', proOnly, (req, res) => {
3726
+ app.get('/api/tunnels/:id/status', (req, res) => {
3725
3727
  const s = daemon.tunnelManager.getStatus(req.params.id);
3726
3728
  if (!s) return res.status(404).json({ error: 'Remote not found' });
3727
3729
  res.json(s);
@@ -3869,27 +3871,6 @@ Keep responses concise. Help them think, don't lecture them about the system the
3869
3871
  res.json({ ok: true });
3870
3872
  });
3871
3873
 
3872
- // --- Project Directory ---
3873
-
3874
- app.post('/api/project-dir', async (req, res) => {
3875
- const { dir } = req.body;
3876
- if (!dir || typeof dir !== 'string') return res.status(400).json({ error: 'dir required' });
3877
- if (/[\0\n\r]/.test(dir)) return res.status(400).json({ error: 'Invalid characters in path' });
3878
- const { existsSync, statSync } = await import('fs');
3879
- const { resolve, isAbsolute } = await import('path');
3880
- const resolved = resolve(dir);
3881
- if (!isAbsolute(resolved)) return res.status(400).json({ error: 'Path must be absolute' });
3882
- if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
3883
- return res.status(400).json({ error: 'Directory does not exist' });
3884
- }
3885
- daemon.config.defaultWorkingDir = resolved;
3886
- const { saveConfig } = await import('./firstrun.js');
3887
- saveConfig(daemon.grooveDir, daemon.config);
3888
- daemon.broadcast({ type: 'config:updated', data: { defaultWorkingDir: resolved } });
3889
- daemon.audit.log('project.dir.change', { dir: resolved });
3890
- res.json({ ok: true, dir: resolved });
3891
- });
3892
-
3893
3874
  // --- Config ---
3894
3875
 
3895
3876
  app.get('/api/config', (req, res) => {
@@ -85,7 +85,7 @@ export class TunnelManager {
85
85
  }));
86
86
  }
87
87
 
88
- save({ name, host, user, port, sshKeyPath, autoStart, autoConnect }) {
88
+ save({ name, host, user, port, sshKeyPath, autoStart, autoConnect, projectDir }) {
89
89
  validateField(name, 'name');
90
90
  validateField(host, 'host');
91
91
  validateField(user, 'user');
@@ -104,6 +104,15 @@ export class TunnelManager {
104
104
  }
105
105
  }
106
106
 
107
+ if (projectDir) {
108
+ if (typeof projectDir !== 'string' || !projectDir.startsWith('/')) {
109
+ throw new Error('projectDir must be an absolute path');
110
+ }
111
+ if (/[;|&`$(){}[\]<>!#\n\r\\]/.test(projectDir)) {
112
+ throw new Error('Invalid characters in projectDir');
113
+ }
114
+ }
115
+
107
116
  const id = crypto.randomUUID().slice(0, 8);
108
117
  const entry = {
109
118
  id,
@@ -114,6 +123,7 @@ export class TunnelManager {
114
123
  sshKeyPath: sshKeyPath || null,
115
124
  autoStart: !!autoStart,
116
125
  autoConnect: !!autoConnect,
126
+ projectDir: projectDir ? projectDir.trim() : null,
117
127
  createdAt: new Date().toISOString(),
118
128
  };
119
129
 
@@ -163,6 +173,19 @@ export class TunnelManager {
163
173
  }
164
174
  if (config.autoStart !== undefined) merged.autoStart = !!config.autoStart;
165
175
  if (config.autoConnect !== undefined) merged.autoConnect = !!config.autoConnect;
176
+ if (config.projectDir !== undefined) {
177
+ if (config.projectDir) {
178
+ if (typeof config.projectDir !== 'string' || !config.projectDir.startsWith('/')) {
179
+ throw new Error('projectDir must be an absolute path');
180
+ }
181
+ if (/[;|&`$(){}[\]<>!#\n\r\\]/.test(config.projectDir)) {
182
+ throw new Error('Invalid characters in projectDir');
183
+ }
184
+ merged.projectDir = config.projectDir.trim();
185
+ } else {
186
+ merged.projectDir = null;
187
+ }
188
+ }
166
189
 
167
190
  this.saved.set(id, merged);
168
191
  this._save();
@@ -380,7 +403,7 @@ export class TunnelManager {
380
403
  '-o', 'ConnectTimeout=10',
381
404
  '-o', 'BatchMode=yes',
382
405
  target,
383
- `bash -lc 'nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 5; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || (echo __DAEMON_FAIL__; tail -20 /tmp/groove-daemon.log 2>/dev/null)'`,
406
+ `bash -lc '${config.projectDir ? `cd "${config.projectDir}" && ` : ''}nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 5; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || (echo __DAEMON_FAIL__; tail -20 /tmp/groove-daemon.log 2>/dev/null)'`,
384
407
  ], {
385
408
  encoding: 'utf8',
386
409
  timeout: 45000,