groove-dev 0.27.7 → 0.27.11
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/node_modules/@groove-dev/daemon/src/api.js +496 -44
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +25 -12
- package/node_modules/@groove-dev/daemon/src/index.js +7 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
- package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
- package/node_modules/@groove-dev/daemon/src/process.js +128 -104
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
- package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/app.css +14 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +16 -17
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
- package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
- package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +150 -6
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +39 -40
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
- package/node_modules/@groove-dev/gui/vite.config.js +3 -0
- package/package.json +7 -2
- package/packages/daemon/src/api.js +496 -44
- package/packages/daemon/src/gateways/manager.js +25 -12
- package/packages/daemon/src/index.js +7 -0
- package/packages/daemon/src/introducer.js +72 -4
- package/packages/daemon/src/journalist.js +66 -11
- package/packages/daemon/src/process.js +128 -104
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/repo-import.js +541 -0
- package/packages/daemon/src/rotator.js +28 -1
- package/packages/daemon/src/supervisor.js +2 -1
- package/packages/daemon/src/tunnel-manager.js +504 -0
- package/packages/daemon/src/validate.js +13 -0
- package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
- package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
- package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
- package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
- package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
- package/packages/gui/src/app.css +14 -0
- package/packages/gui/src/app.jsx +13 -0
- package/packages/gui/src/components/agents/agent-config.jsx +130 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/packages/gui/src/components/agents/agent-node.jsx +16 -17
- package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/packages/gui/src/components/dashboard/intel-panel.jsx +8 -8
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
- package/packages/gui/src/components/layout/app-shell.jsx +7 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/packages/gui/src/components/layout/command-palette.jsx +14 -4
- package/packages/gui/src/components/layout/status-bar.jsx +46 -11
- package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
- package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
- package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
- package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
- package/packages/gui/src/components/ui/toast.jsx +1 -1
- package/packages/gui/src/lib/edition.js +4 -0
- package/packages/gui/src/lib/electron.js +17 -0
- package/packages/gui/src/lib/status.js +1 -0
- package/packages/gui/src/stores/groove.js +150 -6
- package/packages/gui/src/views/dashboard.jsx +39 -40
- package/packages/gui/src/views/marketplace.jsx +82 -0
- package/packages/gui/src/views/settings.jsx +66 -0
- package/packages/gui/vite.config.js +3 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Bl1_J0sN.js +0 -652
- package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +0 -1
- package/packages/gui/dist/assets/index-Bl1_J0sN.js +0 -652
- package/packages/gui/dist/assets/index-DjORRpF0.css +0 -1
- package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
- package/test-slack.mjs +0 -28
- /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// GROOVE — Repo Import (GitHub Clone + Sandbox Isolation)
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync, readdirSync, copyFileSync } from 'fs';
|
|
5
|
+
import { resolve, basename, dirname, isAbsolute } from 'path';
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
const GITHUB_URL_RE = /^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?(?:\/.*)?$/;
|
|
11
|
+
|
|
12
|
+
function parseGitHubUrl(url) {
|
|
13
|
+
const m = url.match(GITHUB_URL_RE);
|
|
14
|
+
if (!m) throw new Error('Invalid GitHub URL. Expected: https://github.com/owner/repo');
|
|
15
|
+
return { owner: m[1], repo: m[2] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class RepoImporter {
|
|
19
|
+
constructor(daemon) {
|
|
20
|
+
this.daemon = daemon;
|
|
21
|
+
this.imports = new Map();
|
|
22
|
+
this.importsDir = resolve(daemon.grooveDir, 'imports');
|
|
23
|
+
mkdirSync(this.importsDir, { recursive: true });
|
|
24
|
+
this._loadExisting();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_loadExisting() {
|
|
28
|
+
try {
|
|
29
|
+
const files = readdirSync(this.importsDir).filter(f => f.endsWith('.json'));
|
|
30
|
+
for (const f of files) {
|
|
31
|
+
try {
|
|
32
|
+
const data = JSON.parse(readFileSync(resolve(this.importsDir, f), 'utf8'));
|
|
33
|
+
if (data.id) this.imports.set(data.id, data);
|
|
34
|
+
} catch { /* skip corrupt manifests */ }
|
|
35
|
+
}
|
|
36
|
+
} catch { /* dir may not exist yet */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_saveManifest(manifest) {
|
|
40
|
+
writeFileSync(
|
|
41
|
+
resolve(this.importsDir, `${manifest.id}.json`),
|
|
42
|
+
JSON.stringify(manifest, null, 2)
|
|
43
|
+
);
|
|
44
|
+
this.imports.set(manifest.id, manifest);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Preview (no clone, no disk writes) ---
|
|
48
|
+
|
|
49
|
+
async preview(repoUrl) {
|
|
50
|
+
const { owner, repo } = parseGitHubUrl(repoUrl);
|
|
51
|
+
|
|
52
|
+
let repoData;
|
|
53
|
+
let treeData;
|
|
54
|
+
|
|
55
|
+
// Try GitHub MCP integration first, fall back to fetch
|
|
56
|
+
const mcp = this.daemon.mcpManager;
|
|
57
|
+
const hasMcp = mcp && typeof mcp.execTool === 'function';
|
|
58
|
+
|
|
59
|
+
if (hasMcp) {
|
|
60
|
+
try {
|
|
61
|
+
repoData = await mcp.execTool('github', 'get_repository', { owner, repo });
|
|
62
|
+
} catch { /* fall through to fetch */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!repoData) {
|
|
66
|
+
const headers = { 'User-Agent': 'groove-dev', Accept: 'application/vnd.github+json' };
|
|
67
|
+
const pat = this._getPat();
|
|
68
|
+
if (pat) headers.Authorization = `Bearer ${pat}`;
|
|
69
|
+
|
|
70
|
+
const resp = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers });
|
|
71
|
+
if (!resp.ok) throw new Error(`GitHub API error: ${resp.status} ${resp.statusText}`);
|
|
72
|
+
repoData = await resp.json();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fetch top-level tree for file detection
|
|
76
|
+
const detectedFiles = [];
|
|
77
|
+
try {
|
|
78
|
+
const headers = { 'User-Agent': 'groove-dev', Accept: 'application/vnd.github+json' };
|
|
79
|
+
const pat = this._getPat();
|
|
80
|
+
if (pat) headers.Authorization = `Bearer ${pat}`;
|
|
81
|
+
|
|
82
|
+
const branch = repoData.default_branch || 'main';
|
|
83
|
+
const treeResp = await fetch(
|
|
84
|
+
`https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=0`,
|
|
85
|
+
{ headers }
|
|
86
|
+
);
|
|
87
|
+
if (treeResp.ok) {
|
|
88
|
+
treeData = await treeResp.json();
|
|
89
|
+
if (treeData.tree) {
|
|
90
|
+
for (const entry of treeData.tree) {
|
|
91
|
+
detectedFiles.push(entry.path);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch { /* tree fetch is optional */ }
|
|
96
|
+
|
|
97
|
+
// Fetch README preview
|
|
98
|
+
let readmePreview = '';
|
|
99
|
+
try {
|
|
100
|
+
const headers = { 'User-Agent': 'groove-dev', Accept: 'application/vnd.github.raw' };
|
|
101
|
+
const pat = this._getPat();
|
|
102
|
+
if (pat) headers.Authorization = `Bearer ${pat}`;
|
|
103
|
+
|
|
104
|
+
const readmeResp = await fetch(
|
|
105
|
+
`https://api.github.com/repos/${owner}/${repo}/readme`,
|
|
106
|
+
{ headers: { ...headers, Accept: 'application/vnd.github+json' } }
|
|
107
|
+
);
|
|
108
|
+
if (readmeResp.ok) {
|
|
109
|
+
const readmeJson = await readmeResp.json();
|
|
110
|
+
if (readmeJson.content) {
|
|
111
|
+
const decoded = Buffer.from(readmeJson.content, 'base64').toString('utf8');
|
|
112
|
+
readmePreview = decoded.slice(0, 800);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch { /* readme is optional */ }
|
|
116
|
+
|
|
117
|
+
const stackHints = this._stackHintsFromFiles(detectedFiles);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
owner,
|
|
121
|
+
repo,
|
|
122
|
+
name: repoData.name || repo,
|
|
123
|
+
description: repoData.description || '',
|
|
124
|
+
language: repoData.language || null,
|
|
125
|
+
stars: repoData.stargazers_count ?? 0,
|
|
126
|
+
license: repoData.license?.spdx_id || repoData.license?.name || null,
|
|
127
|
+
defaultBranch: repoData.default_branch || 'main',
|
|
128
|
+
detectedFiles,
|
|
129
|
+
readmePreview,
|
|
130
|
+
stackHints,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_stackHintsFromFiles(files) {
|
|
135
|
+
const has = (f) => files.includes(f);
|
|
136
|
+
const hints = {};
|
|
137
|
+
if (has('bun.lockb')) { hints.runtime = 'bun'; hints.packageManager = 'bun'; }
|
|
138
|
+
else if (has('pnpm-lock.yaml')) { hints.runtime = 'node'; hints.packageManager = 'pnpm'; }
|
|
139
|
+
else if (has('yarn.lock')) { hints.runtime = 'node'; hints.packageManager = 'yarn'; }
|
|
140
|
+
else if (has('package-lock.json')) { hints.runtime = 'node'; hints.packageManager = 'npm'; }
|
|
141
|
+
else if (has('package.json')) { hints.runtime = 'node'; hints.packageManager = 'npm'; }
|
|
142
|
+
if (has('requirements.txt') || has('pyproject.toml')) hints.runtime = hints.runtime || 'python';
|
|
143
|
+
if (has('go.mod')) hints.runtime = hints.runtime || 'go';
|
|
144
|
+
if (has('Cargo.toml')) hints.runtime = hints.runtime || 'rust';
|
|
145
|
+
if (has('Gemfile')) hints.runtime = hints.runtime || 'ruby';
|
|
146
|
+
if (has('composer.json')) hints.runtime = hints.runtime || 'php';
|
|
147
|
+
if (has('tsconfig.json')) hints.language = 'typescript';
|
|
148
|
+
if (has('docker-compose.yml')) hints.hasDocker = true;
|
|
149
|
+
if (has('.env.example')) hints.hasEnvExample = true;
|
|
150
|
+
return hints;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Stack Detection (on cloned repo) ---
|
|
154
|
+
|
|
155
|
+
detectStack(repoPath) {
|
|
156
|
+
const has = (f) => existsSync(resolve(repoPath, f));
|
|
157
|
+
const read = (f) => {
|
|
158
|
+
try { return readFileSync(resolve(repoPath, f), 'utf8'); } catch { return null; }
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
let runtime = null;
|
|
162
|
+
let language = null;
|
|
163
|
+
let packageManager = null;
|
|
164
|
+
let installCommand = null;
|
|
165
|
+
let buildCommand = null;
|
|
166
|
+
let testCommand = null;
|
|
167
|
+
|
|
168
|
+
if (has('bun.lockb')) {
|
|
169
|
+
runtime = 'bun'; packageManager = 'bun';
|
|
170
|
+
installCommand = 'bun install'; buildCommand = 'bun run build'; testCommand = 'bun test';
|
|
171
|
+
} else if (has('pnpm-lock.yaml')) {
|
|
172
|
+
runtime = 'node'; packageManager = 'pnpm';
|
|
173
|
+
installCommand = 'pnpm install'; buildCommand = 'pnpm build'; testCommand = 'pnpm test';
|
|
174
|
+
} else if (has('yarn.lock')) {
|
|
175
|
+
runtime = 'node'; packageManager = 'yarn';
|
|
176
|
+
installCommand = 'yarn install'; buildCommand = 'yarn build'; testCommand = 'yarn test';
|
|
177
|
+
} else if (has('package-lock.json') || has('package.json')) {
|
|
178
|
+
runtime = 'node'; packageManager = 'npm';
|
|
179
|
+
installCommand = 'npm install'; buildCommand = 'npm run build'; testCommand = 'npm test';
|
|
180
|
+
} else if (has('requirements.txt')) {
|
|
181
|
+
runtime = 'python'; language = 'python';
|
|
182
|
+
installCommand = 'pip install -r requirements.txt';
|
|
183
|
+
} else if (has('pyproject.toml')) {
|
|
184
|
+
runtime = 'python'; language = 'python';
|
|
185
|
+
const content = read('pyproject.toml') || '';
|
|
186
|
+
if (content.includes('[tool.poetry]')) {
|
|
187
|
+
packageManager = 'poetry'; installCommand = 'poetry install';
|
|
188
|
+
} else {
|
|
189
|
+
packageManager = 'uv'; installCommand = 'uv sync';
|
|
190
|
+
}
|
|
191
|
+
} else if (has('go.mod')) {
|
|
192
|
+
runtime = 'go'; language = 'go';
|
|
193
|
+
installCommand = 'go mod download'; buildCommand = 'go build ./...'; testCommand = 'go test ./...';
|
|
194
|
+
} else if (has('Cargo.toml')) {
|
|
195
|
+
runtime = 'rust'; language = 'rust';
|
|
196
|
+
installCommand = 'cargo build'; buildCommand = 'cargo build --release'; testCommand = 'cargo test';
|
|
197
|
+
} else if (has('Gemfile')) {
|
|
198
|
+
runtime = 'ruby'; language = 'ruby';
|
|
199
|
+
installCommand = 'bundle install';
|
|
200
|
+
} else if (has('composer.json')) {
|
|
201
|
+
runtime = 'php'; language = 'php';
|
|
202
|
+
installCommand = 'composer install';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (has('tsconfig.json')) language = 'typescript';
|
|
206
|
+
if (!language && has('package.json')) {
|
|
207
|
+
const pkg = read('package.json');
|
|
208
|
+
if (pkg) {
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(pkg);
|
|
211
|
+
if (parsed.dependencies?.typescript || parsed.devDependencies?.typescript) language = 'typescript';
|
|
212
|
+
} catch { /* skip */ }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!language && runtime === 'node') language = 'javascript';
|
|
216
|
+
|
|
217
|
+
const hasDocker = has('docker-compose.yml');
|
|
218
|
+
const hasEnvExample = has('.env.example');
|
|
219
|
+
|
|
220
|
+
let envVars = [];
|
|
221
|
+
if (hasEnvExample) {
|
|
222
|
+
envVars = this._parseEnvExample(read('.env.example') || '');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
runtime,
|
|
227
|
+
language,
|
|
228
|
+
packageManager,
|
|
229
|
+
hasDocker,
|
|
230
|
+
hasEnvExample,
|
|
231
|
+
envVars,
|
|
232
|
+
installCommand,
|
|
233
|
+
buildCommand,
|
|
234
|
+
testCommand,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_parseEnvExample(content) {
|
|
239
|
+
const vars = [];
|
|
240
|
+
for (const line of content.split('\n')) {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
243
|
+
const eqIdx = trimmed.indexOf('=');
|
|
244
|
+
if (eqIdx < 1) continue;
|
|
245
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
246
|
+
const rest = trimmed.slice(eqIdx + 1).trim();
|
|
247
|
+
let hint = '';
|
|
248
|
+
const commentIdx = rest.indexOf('#');
|
|
249
|
+
if (commentIdx !== -1) hint = rest.slice(commentIdx + 1).trim();
|
|
250
|
+
vars.push({ key, hint });
|
|
251
|
+
}
|
|
252
|
+
return vars;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- Import (clone + sandbox) ---
|
|
256
|
+
|
|
257
|
+
async import(repoUrl, targetPath, options = {}) {
|
|
258
|
+
// Resolve ~ and relative paths
|
|
259
|
+
if (targetPath.startsWith('~/') || targetPath === '~') {
|
|
260
|
+
targetPath = resolve(homedir(), targetPath.slice(2));
|
|
261
|
+
} else if (!isAbsolute(targetPath)) {
|
|
262
|
+
targetPath = resolve(this.daemon.projectDir, targetPath);
|
|
263
|
+
}
|
|
264
|
+
if (targetPath.includes('..')) throw new Error('Path traversal not allowed');
|
|
265
|
+
const parentDir = dirname(targetPath);
|
|
266
|
+
if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
|
|
267
|
+
if (existsSync(targetPath)) throw new Error(`Target path already exists: ${targetPath}`);
|
|
268
|
+
|
|
269
|
+
// Check for duplicate import
|
|
270
|
+
for (const manifest of this.imports.values()) {
|
|
271
|
+
if (manifest.repoUrl === repoUrl && manifest.status === 'active') {
|
|
272
|
+
throw new Error(`Already imported from ${repoUrl} at ${manifest.clonedTo}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const { owner, repo } = parseGitHubUrl(repoUrl);
|
|
277
|
+
const importId = randomUUID().slice(0, 12);
|
|
278
|
+
|
|
279
|
+
// Clone
|
|
280
|
+
const cloneArgs = ['clone', '--depth', '1', repoUrl, targetPath];
|
|
281
|
+
const pat = this._getPat();
|
|
282
|
+
let cloneUrl = repoUrl;
|
|
283
|
+
if (pat && repoUrl.startsWith('https://github.com/')) {
|
|
284
|
+
cloneUrl = repoUrl.replace('https://github.com/', `https://${pat}@github.com/`);
|
|
285
|
+
cloneArgs[3] = cloneUrl;
|
|
286
|
+
}
|
|
287
|
+
execFileSync('git', cloneArgs, { stdio: 'pipe', timeout: 120_000 });
|
|
288
|
+
|
|
289
|
+
// Create sandbox
|
|
290
|
+
this.createSandbox(importId, repoUrl, targetPath);
|
|
291
|
+
|
|
292
|
+
// Take snapshot
|
|
293
|
+
this.takeSnapshot(targetPath);
|
|
294
|
+
|
|
295
|
+
// Detect stack
|
|
296
|
+
const stackInfo = this.detectStack(targetPath);
|
|
297
|
+
|
|
298
|
+
// Save manifest
|
|
299
|
+
const manifest = this.imports.get(importId);
|
|
300
|
+
manifest.owner = owner;
|
|
301
|
+
manifest.repo = repo;
|
|
302
|
+
manifest.name = repo;
|
|
303
|
+
manifest.stackInfo = stackInfo;
|
|
304
|
+
this._saveManifest(manifest);
|
|
305
|
+
|
|
306
|
+
this.daemon.audit?.log('repo.import', { importId, repoUrl, targetPath });
|
|
307
|
+
|
|
308
|
+
return { importId, path: targetPath, stackInfo };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
createSandbox(importId, repoUrl, targetPath) {
|
|
312
|
+
const grooveDir = resolve(targetPath, '.groove');
|
|
313
|
+
mkdirSync(grooveDir, { recursive: true });
|
|
314
|
+
|
|
315
|
+
const manifest = {
|
|
316
|
+
id: importId,
|
|
317
|
+
repoUrl,
|
|
318
|
+
clonedTo: targetPath,
|
|
319
|
+
clonedAt: new Date().toISOString(),
|
|
320
|
+
status: 'active',
|
|
321
|
+
teamId: null,
|
|
322
|
+
agents: [],
|
|
323
|
+
processes: [],
|
|
324
|
+
configsModified: [],
|
|
325
|
+
globalInstalls: [],
|
|
326
|
+
dockerContainers: [],
|
|
327
|
+
credentialKeys: [],
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
writeFileSync(resolve(grooveDir, 'sandbox.json'), JSON.stringify(manifest, null, 2));
|
|
331
|
+
this._saveManifest(manifest);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
takeSnapshot(targetPath) {
|
|
335
|
+
const snapshotDir = resolve(targetPath, '.groove', 'snapshot');
|
|
336
|
+
mkdirSync(snapshotDir, { recursive: true });
|
|
337
|
+
|
|
338
|
+
const projectDir = this.daemon.projectDir;
|
|
339
|
+
const filesToSnapshot = ['package.json', '.mcp.json', 'CLAUDE.md'];
|
|
340
|
+
for (const f of filesToSnapshot) {
|
|
341
|
+
const src = resolve(projectDir, f);
|
|
342
|
+
if (existsSync(src)) {
|
|
343
|
+
copyFileSync(src, resolve(snapshotDir, f));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
recordProcess(importId, pid, command) {
|
|
349
|
+
const manifest = this.imports.get(importId);
|
|
350
|
+
if (!manifest) throw new Error(`Import not found: ${importId}`);
|
|
351
|
+
manifest.processes.push({ pid, command, startedAt: new Date().toISOString() });
|
|
352
|
+
this._saveManifest(manifest);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
recordConfigChange(importId, filePath, originalContent) {
|
|
356
|
+
const manifest = this.imports.get(importId);
|
|
357
|
+
if (!manifest) throw new Error(`Import not found: ${importId}`);
|
|
358
|
+
|
|
359
|
+
const snapshotDir = resolve(manifest.clonedTo, '.groove', 'snapshot');
|
|
360
|
+
mkdirSync(snapshotDir, { recursive: true });
|
|
361
|
+
const safeName = basename(filePath).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
362
|
+
writeFileSync(resolve(snapshotDir, `config-${safeName}`), originalContent);
|
|
363
|
+
|
|
364
|
+
manifest.configsModified.push(filePath);
|
|
365
|
+
this._saveManifest(manifest);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// --- Removal ---
|
|
369
|
+
|
|
370
|
+
async softRemove(importId) {
|
|
371
|
+
const manifest = this.imports.get(importId);
|
|
372
|
+
if (!manifest) throw new Error(`Import not found: ${importId}`);
|
|
373
|
+
|
|
374
|
+
// Kill agents listed in manifest
|
|
375
|
+
for (const agentId of manifest.agents) {
|
|
376
|
+
try { await this.daemon.processes.kill(agentId); } catch { /* already dead */ }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Kill tracked processes
|
|
380
|
+
for (const proc of manifest.processes) {
|
|
381
|
+
try { process.kill(proc.pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Remove credential keys
|
|
385
|
+
if (this.daemon.credentials) {
|
|
386
|
+
for (const key of manifest.credentialKeys) {
|
|
387
|
+
try { this.daemon.credentials.deleteKey(key); } catch { /* ignore */ }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Delete team
|
|
392
|
+
if (manifest.teamId) {
|
|
393
|
+
try { this.daemon.teams.delete(manifest.teamId); } catch { /* ignore */ }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Remove .groove dir inside repo (if repo still exists)
|
|
397
|
+
const repoGrooveDir = resolve(manifest.clonedTo, '.groove');
|
|
398
|
+
if (existsSync(repoGrooveDir)) {
|
|
399
|
+
rmSync(repoGrooveDir, { recursive: true, force: true });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
manifest.status = 'removed';
|
|
403
|
+
this._saveManifest(manifest);
|
|
404
|
+
|
|
405
|
+
this.daemon.audit?.log('repo.softRemove', { importId, clonedTo: manifest.clonedTo });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async hardNuke(importId, { deleteFiles = true } = {}) {
|
|
409
|
+
const manifest = this.imports.get(importId);
|
|
410
|
+
if (!manifest) throw new Error(`Import not found: ${importId}`);
|
|
411
|
+
|
|
412
|
+
// Revert config modifications from snapshot BEFORE softRemove deletes .groove/
|
|
413
|
+
const snapshotDir = resolve(manifest.clonedTo, '.groove', 'snapshot');
|
|
414
|
+
for (const filePath of manifest.configsModified) {
|
|
415
|
+
const safeName = basename(filePath).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
416
|
+
const snapshotPath = resolve(snapshotDir, `config-${safeName}`);
|
|
417
|
+
if (existsSync(snapshotPath)) {
|
|
418
|
+
try {
|
|
419
|
+
const original = readFileSync(snapshotPath, 'utf8');
|
|
420
|
+
writeFileSync(filePath, original);
|
|
421
|
+
} catch { /* best effort */ }
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Soft remove (kills agents, processes, credentials, team, removes .groove/)
|
|
426
|
+
if (manifest.status !== 'removed') {
|
|
427
|
+
await this.softRemove(importId);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!deleteFiles) {
|
|
431
|
+
this.daemon.audit?.log('repo.hardNuke', { importId, clonedTo: manifest.clonedTo, deleteFiles: false });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Rogue process scan
|
|
436
|
+
if (existsSync(manifest.clonedTo)) {
|
|
437
|
+
try {
|
|
438
|
+
const lsofOutput = execFileSync('lsof', ['+D', manifest.clonedTo], {
|
|
439
|
+
stdio: 'pipe', timeout: 10_000,
|
|
440
|
+
}).toString();
|
|
441
|
+
const pids = new Set();
|
|
442
|
+
for (const line of lsofOutput.split('\n').slice(1)) {
|
|
443
|
+
const parts = line.trim().split(/\s+/);
|
|
444
|
+
if (parts[1]) pids.add(parseInt(parts[1], 10));
|
|
445
|
+
}
|
|
446
|
+
for (const pid of pids) {
|
|
447
|
+
if (pid && !isNaN(pid)) {
|
|
448
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* ignore */ }
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} catch { /* lsof may not find anything — that's fine */ }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Delete repo directory
|
|
455
|
+
if (existsSync(manifest.clonedTo)) {
|
|
456
|
+
rmSync(manifest.clonedTo, { recursive: true, force: true });
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Remove import record
|
|
460
|
+
const recordPath = resolve(this.importsDir, `${importId}.json`);
|
|
461
|
+
if (existsSync(recordPath)) {
|
|
462
|
+
rmSync(recordPath);
|
|
463
|
+
}
|
|
464
|
+
this.imports.delete(importId);
|
|
465
|
+
|
|
466
|
+
this.daemon.audit?.log('repo.hardNuke', { importId, clonedTo: manifest.clonedTo });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// --- Queries ---
|
|
470
|
+
|
|
471
|
+
getImported() {
|
|
472
|
+
return Array.from(this.imports.values());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
getImport(importId) {
|
|
476
|
+
return this.imports.get(importId) || null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// --- Setup Agent Prompt ---
|
|
480
|
+
|
|
481
|
+
generateSetupPrompt(repoPath, stackInfo, readmeContent) {
|
|
482
|
+
const lines = [
|
|
483
|
+
`You are setting up an imported repository at: ${repoPath}`,
|
|
484
|
+
'',
|
|
485
|
+
`## Detected Stack`,
|
|
486
|
+
`- Runtime: ${stackInfo.runtime || 'unknown'}`,
|
|
487
|
+
`- Language: ${stackInfo.language || 'unknown'}`,
|
|
488
|
+
`- Package Manager: ${stackInfo.packageManager || 'unknown'}`,
|
|
489
|
+
`- Docker: ${stackInfo.hasDocker ? 'yes' : 'no'}`,
|
|
490
|
+
`- .env.example: ${stackInfo.hasEnvExample ? 'yes' : 'no'}`,
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
if (stackInfo.installCommand) lines.push(`- Install: ${stackInfo.installCommand}`);
|
|
494
|
+
if (stackInfo.buildCommand) lines.push(`- Build: ${stackInfo.buildCommand}`);
|
|
495
|
+
if (stackInfo.testCommand) lines.push(`- Test: ${stackInfo.testCommand}`);
|
|
496
|
+
|
|
497
|
+
if (stackInfo.envVars && stackInfo.envVars.length > 0) {
|
|
498
|
+
lines.push('', '## Environment Variables (from .env.example)');
|
|
499
|
+
for (const v of stackInfo.envVars) {
|
|
500
|
+
lines.push(`- ${v.key}${v.hint ? ` — ${v.hint}` : ''}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (readmeContent) {
|
|
505
|
+
const truncated = readmeContent.slice(0, 4000);
|
|
506
|
+
lines.push('', '## README Content', '', truncated);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
lines.push(
|
|
510
|
+
'',
|
|
511
|
+
'## Playbook',
|
|
512
|
+
'1. Read the README and any setup docs — understand what this project does',
|
|
513
|
+
`2. Install dependencies: ${stackInfo.installCommand || '(detect and run)'}`,
|
|
514
|
+
'3. Check for .env.example — list what the user needs to provide',
|
|
515
|
+
'4. Run setup/init commands if documented',
|
|
516
|
+
'5. Run a build or type-check to verify setup is healthy',
|
|
517
|
+
'6. Summarize: what it is, what it does, how to use it, what is missing',
|
|
518
|
+
'7. Offer to create a team if the user wants ongoing work',
|
|
519
|
+
'',
|
|
520
|
+
'## Safety Rules (MUST FOLLOW)',
|
|
521
|
+
`- Do NOT install anything globally (no npm install -g, pip install --user, brew install)`,
|
|
522
|
+
`- Do NOT modify files outside ${repoPath}/`,
|
|
523
|
+
`- Do NOT run docker commands without asking the user first`,
|
|
524
|
+
`- Do NOT run destructive commands (rm -rf, drop database, etc.)`,
|
|
525
|
+
`- Track every process you spawn via: curl -s -X POST http://localhost:31415/api/repos/<importId>/process -H 'Content-Type: application/json' -d '{"pid":"<PID>","command":"<CMD>"}'`,
|
|
526
|
+
`- If something fails, report the error — do NOT retry destructively`,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
return lines.join('\n');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// --- Helpers ---
|
|
533
|
+
|
|
534
|
+
_getPat() {
|
|
535
|
+
try {
|
|
536
|
+
const creds = this.daemon.credentials;
|
|
537
|
+
if (!creds) return null;
|
|
538
|
+
return creds.getKey?.('github') || creds.getKey?.('github-pat') || null;
|
|
539
|
+
} catch { return null; }
|
|
540
|
+
}
|
|
541
|
+
}
|
|
@@ -6,6 +6,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
|
6
6
|
import { resolve } from 'path';
|
|
7
7
|
|
|
8
8
|
const DEFAULT_THRESHOLD = 0.75;
|
|
9
|
+
const HARD_CEILING = 0.85; // Force rotate at 85% — no idle check, prevents compaction
|
|
9
10
|
const CHECK_INTERVAL = 15_000;
|
|
10
11
|
const QUALITY_THRESHOLD = 40; // Score below this triggers quality rotation
|
|
11
12
|
const MIN_EVENTS = 10; // Minimum classifier events before scoring
|
|
@@ -130,6 +131,13 @@ export class Rotator extends EventEmitter {
|
|
|
130
131
|
for (const agent of running) {
|
|
131
132
|
if (this.rotating.has(agent.id)) continue;
|
|
132
133
|
|
|
134
|
+
// Hard ceiling — force rotate to prevent compaction, even if agent is busy
|
|
135
|
+
if (agent.contextUsage >= HARD_CEILING) {
|
|
136
|
+
console.log(` Rotator: ${agent.name} at ${Math.round(agent.contextUsage * 100)}% — FORCE rotating (hard ceiling)`);
|
|
137
|
+
await this.rotate(agent.id, { reason: 'hard_ceiling' });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
const threshold = this.daemon.adaptive
|
|
134
142
|
? this.daemon.adaptive.getThreshold(agent.provider, agent.role)
|
|
135
143
|
: DEFAULT_THRESHOLD;
|
|
@@ -185,12 +193,29 @@ export class Rotator extends EventEmitter {
|
|
|
185
193
|
delete this.liveScores[agentId];
|
|
186
194
|
delete this.scoreHistory[agentId];
|
|
187
195
|
|
|
188
|
-
let brief = await journalist.generateHandoffBrief(agent
|
|
196
|
+
let brief = await journalist.generateHandoffBrief(agent, {
|
|
197
|
+
reason: options.reason,
|
|
198
|
+
qualityScore: options.qualityScore,
|
|
199
|
+
signals: options.signals,
|
|
200
|
+
});
|
|
189
201
|
|
|
190
202
|
if (options.additionalPrompt) {
|
|
191
203
|
brief = brief + '\n\n## User Instruction\n\n' + options.additionalPrompt;
|
|
192
204
|
}
|
|
193
205
|
|
|
206
|
+
// Persist to Layer 7 handoff chain so future rotations have causal continuity
|
|
207
|
+
if (this.daemon.memory) {
|
|
208
|
+
this.daemon.memory.appendHandoffBrief(agent.role, {
|
|
209
|
+
timestamp: new Date().toISOString(),
|
|
210
|
+
agentId: agent.id,
|
|
211
|
+
newAgentId: null, // filled after respawn completes
|
|
212
|
+
reason: options.reason || 'manual',
|
|
213
|
+
oldTokens: agent.tokensUsed,
|
|
214
|
+
contextUsage: agent.contextUsage,
|
|
215
|
+
brief: brief.slice(0, 4000),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
194
219
|
const record = {
|
|
195
220
|
agentId: agent.id,
|
|
196
221
|
agentName: agent.name,
|
|
@@ -332,6 +357,7 @@ export class Rotator extends EventEmitter {
|
|
|
332
357
|
const qualityRotations = this.rotationHistory.filter((r) => r.reason === 'quality_degradation').length;
|
|
333
358
|
const contextRotations = this.rotationHistory.filter((r) => r.reason === 'context_threshold').length;
|
|
334
359
|
const naturalCompactions = this.rotationHistory.filter((r) => r.reason === 'natural_compaction').length;
|
|
360
|
+
const hardCeilingRotations = this.rotationHistory.filter((r) => r.reason === 'hard_ceiling').length;
|
|
335
361
|
return {
|
|
336
362
|
enabled: this.enabled,
|
|
337
363
|
totalRotations,
|
|
@@ -339,6 +365,7 @@ export class Rotator extends EventEmitter {
|
|
|
339
365
|
qualityRotations,
|
|
340
366
|
contextRotations,
|
|
341
367
|
naturalCompactions,
|
|
368
|
+
hardCeilingRotations,
|
|
342
369
|
rotating: Array.from(this.rotating),
|
|
343
370
|
liveScores: this.liveScores,
|
|
344
371
|
scoreHistory: this.scoreHistory,
|
|
@@ -18,7 +18,7 @@ export class Supervisor {
|
|
|
18
18
|
|
|
19
19
|
// --- Approval System ---
|
|
20
20
|
|
|
21
|
-
requestApproval(agentId, action) {
|
|
21
|
+
requestApproval(agentId, action, retryPayload = null) {
|
|
22
22
|
const id = `approval-${Date.now()}-${++this.counter}`;
|
|
23
23
|
const agent = this.daemon.registry?.get(agentId);
|
|
24
24
|
|
|
@@ -30,6 +30,7 @@ export class Supervisor {
|
|
|
30
30
|
status: 'pending',
|
|
31
31
|
requestedAt: new Date().toISOString(),
|
|
32
32
|
};
|
|
33
|
+
if (retryPayload) approval.retryPayload = retryPayload;
|
|
33
34
|
|
|
34
35
|
this.pendingApprovals.set(id, approval);
|
|
35
36
|
this.daemon.broadcast({ type: 'approval:request', data: approval });
|