groove-dev 0.27.32 → 0.27.33
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/start.js +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +28 -1
- package/node_modules/@groove-dev/daemon/src/firstrun.js +1 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +16 -4
- package/node_modules/@groove-dev/daemon/src/memory.js +6 -1
- package/node_modules/@groove-dev/daemon/src/process.js +44 -28
- package/node_modules/@groove-dev/daemon/src/rotator.js +1 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +19 -7
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
- 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/layout/app-shell.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
- package/node_modules/@groove-dev/gui/src/components/layout/project-picker.jsx +127 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +22 -15
- package/node_modules/@groove-dev/gui/src/stores/groove.js +39 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/start.js +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +28 -1
- package/packages/daemon/src/firstrun.js +1 -0
- package/packages/daemon/src/index.js +14 -0
- package/packages/daemon/src/journalist.js +16 -4
- package/packages/daemon/src/memory.js +6 -1
- package/packages/daemon/src/process.js +44 -28
- package/packages/daemon/src/rotator.js +1 -0
- package/packages/daemon/src/tunnel-manager.js +19 -7
- package/packages/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
- package/packages/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/layout/app-shell.jsx +2 -0
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
- package/packages/gui/src/components/layout/project-picker.jsx +127 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +22 -15
- package/packages/gui/src/stores/groove.js +39 -2
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BoU6IhQI.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BnLiWvrh.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -13,6 +13,7 @@ import { DetailPanel } from './detail-panel';
|
|
|
13
13
|
import { CommandPalette } from './command-palette';
|
|
14
14
|
import { ApprovalModal } from '../ui/approval-modal';
|
|
15
15
|
import { QuickConnect } from '../settings/quick-connect';
|
|
16
|
+
import { ProjectPicker } from './project-picker';
|
|
16
17
|
import { TeamTabBar } from '../../views/agents';
|
|
17
18
|
|
|
18
19
|
export function AppShell({ children, detailContent, terminalContent }) {
|
|
@@ -117,6 +118,7 @@ export function AppShell({ children, detailContent, terminalContent }) {
|
|
|
117
118
|
|
|
118
119
|
<CommandPalette />
|
|
119
120
|
<QuickConnect />
|
|
121
|
+
<ProjectPicker />
|
|
120
122
|
<ApprovalModal />
|
|
121
123
|
<ToastContainer />
|
|
122
124
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useEffect, useRef } from 'react';
|
|
3
|
-
import { Search, ChevronRight, LogIn, LogOut, User, ExternalLink, BookOpen, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { Search, ChevronRight, LogIn, LogOut, User, ExternalLink, BookOpen, ChevronDown, FolderOpen } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { useGrooveStore } from '../../stores/groove';
|
|
6
6
|
import { isElectron, getPlatform } from '../../lib/electron';
|
|
@@ -130,10 +130,13 @@ const VIEW_LABELS = {
|
|
|
130
130
|
export function BreadcrumbBar({
|
|
131
131
|
activeView,
|
|
132
132
|
connected,
|
|
133
|
+
tunneled,
|
|
133
134
|
daemonHost,
|
|
134
135
|
editorActiveFile,
|
|
135
136
|
onOpenCommandPalette,
|
|
136
137
|
}) {
|
|
138
|
+
const projectDir = useGrooveStore((s) => s.projectDir);
|
|
139
|
+
const toggleProjectPicker = useGrooveStore((s) => s.toggleProjectPicker);
|
|
137
140
|
const crumbs = ['Groove', VIEW_LABELS[activeView] || activeView];
|
|
138
141
|
if (activeView === 'editor' && editorActiveFile) {
|
|
139
142
|
crumbs.push(editorActiveFile.split('/').pop());
|
|
@@ -182,6 +185,18 @@ export function BreadcrumbBar({
|
|
|
182
185
|
</span>
|
|
183
186
|
)}
|
|
184
187
|
|
|
188
|
+
{/* Project dir badge — remote sessions, clickable to change */}
|
|
189
|
+
{tunneled && projectDir && (
|
|
190
|
+
<button
|
|
191
|
+
onClick={toggleProjectPicker}
|
|
192
|
+
className="flex items-center gap-1 text-2xs font-mono font-medium text-text-2 bg-surface-5 px-1.5 py-0.5 rounded flex-shrink-0 hover:bg-surface-4 hover:text-text-0 transition-colors cursor-pointer"
|
|
193
|
+
title={projectDir}
|
|
194
|
+
>
|
|
195
|
+
<FolderOpen size={11} />
|
|
196
|
+
{projectDir.split('/').pop() || '/'}
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
|
|
185
200
|
<div className="flex-1 min-w-4" />
|
|
186
201
|
|
|
187
202
|
{/* Breadcrumbs */}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { FolderBrowser } from '../agents/folder-browser';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import {
|
|
7
|
+
FolderOpen, FolderClosed, Clock, ChevronRight, Plus, Monitor,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
|
|
10
|
+
function formatTimeAgo(iso) {
|
|
11
|
+
if (!iso) return '';
|
|
12
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
13
|
+
const mins = Math.floor(diff / 60000);
|
|
14
|
+
if (mins < 1) return 'just now';
|
|
15
|
+
if (mins < 60) return `${mins}m ago`;
|
|
16
|
+
const hours = Math.floor(mins / 60);
|
|
17
|
+
if (hours < 24) return `${hours}h ago`;
|
|
18
|
+
const days = Math.floor(hours / 24);
|
|
19
|
+
return `${days}d ago`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ProjectPicker() {
|
|
23
|
+
const show = useGrooveStore((s) => s.showProjectPicker);
|
|
24
|
+
const recentProjects = useGrooveStore((s) => s.recentProjects);
|
|
25
|
+
const setProjectDir = useGrooveStore((s) => s.setProjectDir);
|
|
26
|
+
const [browserOpen, setBrowserOpen] = useState(false);
|
|
27
|
+
const [loading, setLoading] = useState(null);
|
|
28
|
+
|
|
29
|
+
if (!show) return null;
|
|
30
|
+
|
|
31
|
+
async function handleSelect(path) {
|
|
32
|
+
setLoading(path);
|
|
33
|
+
try {
|
|
34
|
+
await setProjectDir(path);
|
|
35
|
+
} catch {
|
|
36
|
+
setLoading(null);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function handleBrowseSelect(path) {
|
|
41
|
+
setBrowserOpen(false);
|
|
42
|
+
await handleSelect(path);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-surface-0/90 backdrop-blur-sm">
|
|
47
|
+
<div className="w-full max-w-lg mx-4">
|
|
48
|
+
{/* Header */}
|
|
49
|
+
<div className="text-center mb-8">
|
|
50
|
+
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-accent/10 mb-4">
|
|
51
|
+
<Monitor size={28} className="text-accent" />
|
|
52
|
+
</div>
|
|
53
|
+
<h1 className="text-xl font-semibold text-text-0 mb-1">Open a project</h1>
|
|
54
|
+
<p className="text-sm text-text-3">Select a working directory for this session</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{/* Recent projects */}
|
|
58
|
+
{recentProjects.length > 0 && (
|
|
59
|
+
<div className="mb-4">
|
|
60
|
+
<div className="flex items-center gap-2 mb-2 px-1">
|
|
61
|
+
<Clock size={13} className="text-text-4" />
|
|
62
|
+
<span className="text-xs font-medium text-text-3 uppercase tracking-wider">Recent</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="bg-surface-2 rounded-xl border border-border overflow-hidden">
|
|
65
|
+
{recentProjects.map((project, i) => (
|
|
66
|
+
<button
|
|
67
|
+
key={project.path}
|
|
68
|
+
onClick={() => handleSelect(project.path)}
|
|
69
|
+
disabled={loading !== null}
|
|
70
|
+
className={cn(
|
|
71
|
+
'w-full flex items-center gap-3 px-4 py-3 text-left cursor-pointer',
|
|
72
|
+
'hover:bg-surface-4 transition-colors',
|
|
73
|
+
'disabled:opacity-50 disabled:cursor-wait',
|
|
74
|
+
i < recentProjects.length - 1 && 'border-b border-border-subtle',
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<FolderClosed size={18} className="text-warning flex-shrink-0" />
|
|
78
|
+
<div className="flex-1 min-w-0">
|
|
79
|
+
<div className="text-sm font-medium text-text-0 truncate">{project.name}</div>
|
|
80
|
+
<div className="text-xs text-text-3 font-mono truncate">{project.path}</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
83
|
+
{project.openedAt && (
|
|
84
|
+
<span className="text-[11px] text-text-4">{formatTimeAgo(project.openedAt)}</span>
|
|
85
|
+
)}
|
|
86
|
+
{loading === project.path ? (
|
|
87
|
+
<div className="w-4 h-4 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
|
88
|
+
) : (
|
|
89
|
+
<ChevronRight size={14} className="text-text-4" />
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</button>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{/* Open folder button */}
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => setBrowserOpen(true)}
|
|
101
|
+
disabled={loading !== null}
|
|
102
|
+
className={cn(
|
|
103
|
+
'w-full flex items-center gap-3 px-4 py-3.5 rounded-xl cursor-pointer',
|
|
104
|
+
'bg-surface-2 border border-border border-dashed',
|
|
105
|
+
'hover:bg-surface-4 hover:border-accent/30 transition-colors',
|
|
106
|
+
'disabled:opacity-50 disabled:cursor-wait',
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<div className="flex items-center justify-center w-9 h-9 rounded-lg bg-accent/10">
|
|
110
|
+
<Plus size={18} className="text-accent" />
|
|
111
|
+
</div>
|
|
112
|
+
<div className="text-left">
|
|
113
|
+
<div className="text-sm font-medium text-text-0">Open Folder</div>
|
|
114
|
+
<div className="text-xs text-text-3">Browse the filesystem</div>
|
|
115
|
+
</div>
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
<FolderBrowser
|
|
119
|
+
open={browserOpen}
|
|
120
|
+
onOpenChange={setBrowserOpen}
|
|
121
|
+
currentPath="/home"
|
|
122
|
+
onSelect={handleBrowseSelect}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
@@ -17,6 +17,7 @@ export function QuickConnect() {
|
|
|
17
17
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
18
18
|
const [connectingId, setConnectingId] = useState(null);
|
|
19
19
|
const [showWizard, setShowWizard] = useState(false);
|
|
20
|
+
const wizardTunnelId = useRef(null);
|
|
20
21
|
|
|
21
22
|
if (!open) return null;
|
|
22
23
|
|
|
@@ -25,7 +26,9 @@ export function QuickConnect() {
|
|
|
25
26
|
try {
|
|
26
27
|
await useGrooveStore.getState().connectTunnel(id);
|
|
27
28
|
toggle();
|
|
28
|
-
} catch {
|
|
29
|
+
} catch (err) {
|
|
30
|
+
addToast('error', 'Connection failed', err?.message || 'Unknown error');
|
|
31
|
+
}
|
|
29
32
|
setConnectingId(null);
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -81,24 +84,28 @@ export function QuickConnect() {
|
|
|
81
84
|
<SSHWizard
|
|
82
85
|
server={null}
|
|
83
86
|
onSave={async (data) => {
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
const existingId = data.id || wizardTunnelId.current;
|
|
88
|
+
if (existingId) {
|
|
89
|
+
await useGrooveStore.getState().updateTunnel(existingId, data);
|
|
90
|
+
addToast('success', 'Server updated');
|
|
86
91
|
} else {
|
|
87
|
-
await useGrooveStore.getState().saveTunnel(data);
|
|
92
|
+
const result = await useGrooveStore.getState().saveTunnel(data);
|
|
93
|
+
if (result?.id) wizardTunnelId.current = result.id;
|
|
94
|
+
addToast('success', 'Server added');
|
|
88
95
|
}
|
|
89
|
-
addToast('success', data.id ? 'Server updated' : 'Server added');
|
|
90
96
|
}}
|
|
91
97
|
onTest={() => {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
if (last?.id) return useGrooveStore.getState().testTunnel(last.id);
|
|
98
|
+
const id = wizardTunnelId.current;
|
|
99
|
+
if (id) return useGrooveStore.getState().testTunnel(id);
|
|
95
100
|
}}
|
|
96
101
|
onConnect={() => {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
const id = wizardTunnelId.current;
|
|
103
|
+
if (id) return useGrooveStore.getState().connectTunnel(id);
|
|
104
|
+
}}
|
|
105
|
+
onCancel={() => {
|
|
106
|
+
wizardTunnelId.current = null;
|
|
107
|
+
setShowWizard(false);
|
|
100
108
|
}}
|
|
101
|
-
onCancel={() => setShowWizard(false)}
|
|
102
109
|
/>
|
|
103
110
|
) : (
|
|
104
111
|
<>
|
|
@@ -112,7 +119,7 @@ export function QuickConnect() {
|
|
|
112
119
|
<Button
|
|
113
120
|
variant="primary"
|
|
114
121
|
size="sm"
|
|
115
|
-
onClick={() => setShowWizard(true)}
|
|
122
|
+
onClick={() => { wizardTunnelId.current = null; setShowWizard(true); }}
|
|
116
123
|
className="h-8 text-xs gap-1.5 mt-3"
|
|
117
124
|
>
|
|
118
125
|
<Plus size={12} /> Add Connection
|
|
@@ -157,7 +164,7 @@ export function QuickConnect() {
|
|
|
157
164
|
{/* Footer with Add button */}
|
|
158
165
|
<div className="px-4 py-2.5 border-t border-border-subtle">
|
|
159
166
|
<button
|
|
160
|
-
onClick={() => setShowWizard(true)}
|
|
167
|
+
onClick={() => { wizardTunnelId.current = null; setShowWizard(true); }}
|
|
161
168
|
className="flex items-center gap-1.5 text-2xs text-accent hover:text-accent/80 font-sans font-medium cursor-pointer transition-colors"
|
|
162
169
|
>
|
|
163
170
|
<Plus size={10} /> Add new connection
|
|
@@ -107,6 +107,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
107
107
|
// ── Toasts ────────────────────────────────────────────────
|
|
108
108
|
toasts: [],
|
|
109
109
|
|
|
110
|
+
// ── Project Directory ───────────────────────────────────────
|
|
111
|
+
projectDir: null,
|
|
112
|
+
recentProjects: [],
|
|
113
|
+
showProjectPicker: false,
|
|
114
|
+
|
|
110
115
|
// ── Tunnels ────────────────────────────────────────────────
|
|
111
116
|
savedTunnels: [],
|
|
112
117
|
activeTunnelId: null,
|
|
@@ -140,9 +145,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
140
145
|
const updates = {};
|
|
141
146
|
if (s.host && s.host !== '127.0.0.1') updates.daemonHost = s.host;
|
|
142
147
|
const browserPort = window.location.port || '80';
|
|
143
|
-
|
|
148
|
+
const isTunneled = String(s.port) !== browserPort;
|
|
149
|
+
if (isTunneled) updates.tunneled = true;
|
|
144
150
|
if (s.version) updates.version = s.version;
|
|
145
151
|
if (Object.keys(updates).length > 0) set(updates);
|
|
152
|
+
if (isTunneled) get().fetchProjectDir();
|
|
146
153
|
}).catch(() => {});
|
|
147
154
|
get().fetchTeams();
|
|
148
155
|
get().fetchApprovals();
|
|
@@ -502,6 +509,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
502
509
|
}));
|
|
503
510
|
break;
|
|
504
511
|
|
|
512
|
+
case 'project-dir:changed':
|
|
513
|
+
set({ projectDir: msg.data?.projectDir, showProjectPicker: false });
|
|
514
|
+
break;
|
|
515
|
+
|
|
505
516
|
case 'tunnel.connected':
|
|
506
517
|
set({ activeTunnelId: msg.data?.id || null });
|
|
507
518
|
get().fetchTunnels();
|
|
@@ -1034,6 +1045,33 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1034
1045
|
get().fetchImportedRepos();
|
|
1035
1046
|
},
|
|
1036
1047
|
|
|
1048
|
+
// ── Project Directory ────────────────────────────────────
|
|
1049
|
+
|
|
1050
|
+
async fetchProjectDir() {
|
|
1051
|
+
try {
|
|
1052
|
+
const data = await api.get('/project-dir');
|
|
1053
|
+
const isHome = /^\/home\/[^/]+$/.test(data.projectDir) || data.projectDir === '/root';
|
|
1054
|
+
set({
|
|
1055
|
+
projectDir: data.projectDir,
|
|
1056
|
+
recentProjects: data.recentProjects || [],
|
|
1057
|
+
showProjectPicker: isHome || (data.recentProjects || []).length === 0,
|
|
1058
|
+
});
|
|
1059
|
+
} catch {}
|
|
1060
|
+
},
|
|
1061
|
+
|
|
1062
|
+
async setProjectDir(path) {
|
|
1063
|
+
const data = await api.post('/project-dir', { path });
|
|
1064
|
+
set({
|
|
1065
|
+
projectDir: data.projectDir,
|
|
1066
|
+
recentProjects: data.recentProjects || [],
|
|
1067
|
+
showProjectPicker: false,
|
|
1068
|
+
});
|
|
1069
|
+
},
|
|
1070
|
+
|
|
1071
|
+
toggleProjectPicker() {
|
|
1072
|
+
set((s) => ({ showProjectPicker: !s.showProjectPicker }));
|
|
1073
|
+
},
|
|
1074
|
+
|
|
1037
1075
|
// ── Tunnels ──────────────────────────────────────────────
|
|
1038
1076
|
|
|
1039
1077
|
async fetchTunnels() {
|
|
@@ -1068,7 +1106,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1068
1106
|
const result = await api.post(`/tunnels/${encodeURIComponent(id)}/connect`);
|
|
1069
1107
|
set({ activeTunnelId: id });
|
|
1070
1108
|
get().fetchTunnels();
|
|
1071
|
-
if (result.url) window.open(result.url, '_blank');
|
|
1072
1109
|
return result;
|
|
1073
1110
|
},
|
|
1074
1111
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.33",
|
|
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)",
|
|
@@ -19,7 +19,7 @@ export async function start(options) {
|
|
|
19
19
|
setupKeys = result.keys || {};
|
|
20
20
|
} catch (err) {
|
|
21
21
|
// If stdin is not interactive (piped), skip wizard
|
|
22
|
-
if (err.code === 'ERR_USE_AFTER_CLOSE') {
|
|
22
|
+
if (err.code === 'ERR_USE_AFTER_CLOSE' || !process.stdin.isTTY) {
|
|
23
23
|
console.log(chalk.dim(' Non-interactive mode — skipping setup wizard.'));
|
|
24
24
|
} else {
|
|
25
25
|
throw err;
|
|
@@ -695,6 +695,28 @@ export function createApi(app, daemon) {
|
|
|
695
695
|
});
|
|
696
696
|
});
|
|
697
697
|
|
|
698
|
+
// --- Project Directory ---
|
|
699
|
+
|
|
700
|
+
app.get('/api/project-dir', (req, res) => {
|
|
701
|
+
res.json({
|
|
702
|
+
projectDir: daemon.projectDir,
|
|
703
|
+
recentProjects: daemon.config.recentProjects || [],
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
app.post('/api/project-dir', (req, res) => {
|
|
708
|
+
const { path: dirPath } = req.body || {};
|
|
709
|
+
if (!dirPath || typeof dirPath !== 'string') {
|
|
710
|
+
return res.status(400).json({ error: 'path is required' });
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
daemon.setProjectDir(dirPath);
|
|
714
|
+
res.json({ projectDir: daemon.projectDir, recentProjects: daemon.config.recentProjects || [] });
|
|
715
|
+
} catch (err) {
|
|
716
|
+
res.status(400).json({ error: err.message });
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
698
720
|
// --- Teams (live agent groups) ---
|
|
699
721
|
|
|
700
722
|
app.get('/api/teams', (req, res) => {
|
|
@@ -3330,7 +3352,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3330
3352
|
|
|
3331
3353
|
app.post('/api/tunnels/:id/connect', proOnly, async (req, res) => {
|
|
3332
3354
|
try {
|
|
3333
|
-
const
|
|
3355
|
+
const opts = {};
|
|
3356
|
+
if (req.body?.skipTest && req.body?.testResult) {
|
|
3357
|
+
opts.skipTest = true;
|
|
3358
|
+
opts.testResult = req.body.testResult;
|
|
3359
|
+
}
|
|
3360
|
+
const result = await daemon.tunnelManager.connect(req.params.id, opts);
|
|
3334
3361
|
res.json(result);
|
|
3335
3362
|
} catch (err) {
|
|
3336
3363
|
const body = { error: err.message };
|
|
@@ -337,6 +337,20 @@ export class Daemon {
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
setProjectDir(dirPath) {
|
|
341
|
+
if (!dirPath || typeof dirPath !== 'string') throw new Error('Invalid path');
|
|
342
|
+
const resolved = resolve(dirPath);
|
|
343
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
344
|
+
throw new Error('Directory does not exist');
|
|
345
|
+
}
|
|
346
|
+
this.projectDir = resolved;
|
|
347
|
+
const recents = (this.config.recentProjects || []).filter((r) => r.path !== resolved);
|
|
348
|
+
recents.unshift({ path: resolved, name: resolved.split('/').pop(), openedAt: new Date().toISOString() });
|
|
349
|
+
this.config.recentProjects = recents.slice(0, 10);
|
|
350
|
+
saveConfig(this.grooveDir, this.config);
|
|
351
|
+
this.broadcast({ type: 'project-dir:changed', data: { projectDir: resolved } });
|
|
352
|
+
}
|
|
353
|
+
|
|
340
354
|
async _pollSubscription() {
|
|
341
355
|
if (!this.authToken) return;
|
|
342
356
|
const API_BASE = 'https://docs.groovedev.ai/api/v1';
|
|
@@ -22,6 +22,7 @@ export class Journalist {
|
|
|
22
22
|
this.history = []; // recent synthesis summaries
|
|
23
23
|
this._debounceTimer = null;
|
|
24
24
|
this._debounceReason = null;
|
|
25
|
+
this._forceNextCycle = false;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
start(intervalMs = DEFAULT_INTERVAL) {
|
|
@@ -61,6 +62,9 @@ export class Journalist {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
requestSynthesis(reason = 'unknown') {
|
|
65
|
+
if (reason === 'completion' || reason === 'rotation_complete') {
|
|
66
|
+
this._forceNextCycle = true;
|
|
67
|
+
}
|
|
64
68
|
if (this._debounceTimer) {
|
|
65
69
|
this._debounceReason = reason;
|
|
66
70
|
return;
|
|
@@ -84,7 +88,10 @@ export class Journalist {
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
async cycle() {
|
|
87
|
-
if (this.synthesizing)
|
|
91
|
+
if (this.synthesizing) {
|
|
92
|
+
console.log(' Journalist: skipping cycle (already synthesizing)');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
88
95
|
|
|
89
96
|
const agents = this.daemon.registry.getAll();
|
|
90
97
|
const running = agents.filter((a) => a.status === 'running');
|
|
@@ -99,14 +106,19 @@ export class Journalist {
|
|
|
99
106
|
const activeAgents = [...running, ...recentlyCompleted];
|
|
100
107
|
|
|
101
108
|
// Skip if no agents to synthesize
|
|
102
|
-
if (activeAgents.length === 0)
|
|
109
|
+
if (activeAgents.length === 0) {
|
|
110
|
+
console.log(' Journalist: skipping cycle (no active agents)');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
103
113
|
|
|
104
|
-
// Smart scheduling: skip if no new log output since last cycle
|
|
105
|
-
if (this.lastCycleAt && !this.hasNewActivity(activeAgents)) {
|
|
114
|
+
// Smart scheduling: skip if no new log output since last cycle (unless forced by completion/rotation)
|
|
115
|
+
if (this.lastCycleAt && !this._forceNextCycle && !this.hasNewActivity(activeAgents)) {
|
|
116
|
+
console.log(' Journalist: skipping cycle (no new activity)');
|
|
106
117
|
return;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
this.synthesizing = true;
|
|
121
|
+
this._forceNextCycle = false;
|
|
110
122
|
this.cycleCount++;
|
|
111
123
|
this.lastCycleAt = Date.now();
|
|
112
124
|
|
|
@@ -165,12 +165,13 @@ export class MemoryStore {
|
|
|
165
165
|
const entries = [];
|
|
166
166
|
const blocks = content.split(/\n(?=## Rotation )/);
|
|
167
167
|
for (const block of blocks) {
|
|
168
|
-
const headerMatch = block.match(/^## Rotation (\d+)
|
|
168
|
+
const headerMatch = block.match(/^## Rotation (\d+) — [^(]*\(([\w?-]+) →/);
|
|
169
169
|
if (!headerMatch) continue;
|
|
170
170
|
const body = block.replace(/\n---\s*$/, '').trim();
|
|
171
171
|
entries.push({
|
|
172
172
|
rotationN: parseInt(headerMatch[1], 10),
|
|
173
173
|
body,
|
|
174
|
+
agentId: headerMatch[2] || null,
|
|
174
175
|
});
|
|
175
176
|
}
|
|
176
177
|
return entries;
|
|
@@ -182,6 +183,10 @@ export class MemoryStore {
|
|
|
182
183
|
appendHandoffBrief(role, entry, workingDir, teamId) {
|
|
183
184
|
if (!role || !entry) return false;
|
|
184
185
|
const chain = this.getHandoffChain(role, workingDir, teamId);
|
|
186
|
+
|
|
187
|
+
// Dedup: prevent the same agent from having multiple entries in the chain
|
|
188
|
+
if (entry.agentId && chain.some(c => c.agentId === entry.agentId)) return false;
|
|
189
|
+
|
|
185
190
|
const nextN = (chain[0]?.rotationN || 0) + 1;
|
|
186
191
|
|
|
187
192
|
const block = [
|