groove-dev 0.8.0

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 (84) hide show
  1. package/CLAUDE.md +197 -0
  2. package/LICENSE +40 -0
  3. package/README.md +115 -0
  4. package/docs/GUI_DESIGN_SPEC.md +402 -0
  5. package/favicon.png +0 -0
  6. package/groove-logo-short.png +0 -0
  7. package/groove-logo.png +0 -0
  8. package/package.json +70 -0
  9. package/packages/cli/bin/groove.js +98 -0
  10. package/packages/cli/package.json +15 -0
  11. package/packages/cli/src/client.js +25 -0
  12. package/packages/cli/src/commands/agents.js +38 -0
  13. package/packages/cli/src/commands/approve.js +50 -0
  14. package/packages/cli/src/commands/config.js +35 -0
  15. package/packages/cli/src/commands/kill.js +15 -0
  16. package/packages/cli/src/commands/nuke.js +19 -0
  17. package/packages/cli/src/commands/providers.js +40 -0
  18. package/packages/cli/src/commands/rotate.js +16 -0
  19. package/packages/cli/src/commands/spawn.js +91 -0
  20. package/packages/cli/src/commands/start.js +31 -0
  21. package/packages/cli/src/commands/status.js +38 -0
  22. package/packages/cli/src/commands/stop.js +15 -0
  23. package/packages/cli/src/commands/team.js +77 -0
  24. package/packages/daemon/package.json +18 -0
  25. package/packages/daemon/src/adaptive.js +237 -0
  26. package/packages/daemon/src/api.js +533 -0
  27. package/packages/daemon/src/classifier.js +126 -0
  28. package/packages/daemon/src/credentials.js +121 -0
  29. package/packages/daemon/src/firstrun.js +93 -0
  30. package/packages/daemon/src/index.js +208 -0
  31. package/packages/daemon/src/introducer.js +238 -0
  32. package/packages/daemon/src/journalist.js +600 -0
  33. package/packages/daemon/src/lockmanager.js +58 -0
  34. package/packages/daemon/src/pm.js +108 -0
  35. package/packages/daemon/src/process.js +361 -0
  36. package/packages/daemon/src/providers/aider.js +72 -0
  37. package/packages/daemon/src/providers/base.js +38 -0
  38. package/packages/daemon/src/providers/claude-code.js +167 -0
  39. package/packages/daemon/src/providers/codex.js +68 -0
  40. package/packages/daemon/src/providers/gemini.js +62 -0
  41. package/packages/daemon/src/providers/index.js +38 -0
  42. package/packages/daemon/src/providers/ollama.js +94 -0
  43. package/packages/daemon/src/registry.js +89 -0
  44. package/packages/daemon/src/rotator.js +185 -0
  45. package/packages/daemon/src/router.js +132 -0
  46. package/packages/daemon/src/state.js +34 -0
  47. package/packages/daemon/src/supervisor.js +178 -0
  48. package/packages/daemon/src/teams.js +203 -0
  49. package/packages/daemon/src/terminal/base.js +27 -0
  50. package/packages/daemon/src/terminal/generic.js +27 -0
  51. package/packages/daemon/src/terminal/tmux.js +64 -0
  52. package/packages/daemon/src/tokentracker.js +124 -0
  53. package/packages/daemon/src/validate.js +122 -0
  54. package/packages/daemon/templates/api-builder.json +18 -0
  55. package/packages/daemon/templates/fullstack.json +18 -0
  56. package/packages/daemon/templates/monorepo.json +24 -0
  57. package/packages/gui/dist/assets/index-BO95Rm1F.js +73 -0
  58. package/packages/gui/dist/assets/index-CPzm9ZE9.css +1 -0
  59. package/packages/gui/dist/favicon.png +0 -0
  60. package/packages/gui/dist/groove-logo-short.png +0 -0
  61. package/packages/gui/dist/groove-logo.png +0 -0
  62. package/packages/gui/dist/index.html +13 -0
  63. package/packages/gui/index.html +12 -0
  64. package/packages/gui/package.json +22 -0
  65. package/packages/gui/public/favicon.png +0 -0
  66. package/packages/gui/public/groove-logo-short.png +0 -0
  67. package/packages/gui/public/groove-logo.png +0 -0
  68. package/packages/gui/src/App.jsx +215 -0
  69. package/packages/gui/src/components/AgentActions.jsx +347 -0
  70. package/packages/gui/src/components/AgentChat.jsx +479 -0
  71. package/packages/gui/src/components/AgentNode.jsx +117 -0
  72. package/packages/gui/src/components/AgentPanel.jsx +115 -0
  73. package/packages/gui/src/components/AgentStats.jsx +333 -0
  74. package/packages/gui/src/components/ApprovalQueue.jsx +156 -0
  75. package/packages/gui/src/components/EmptyState.jsx +100 -0
  76. package/packages/gui/src/components/SpawnPanel.jsx +515 -0
  77. package/packages/gui/src/components/TeamSelector.jsx +162 -0
  78. package/packages/gui/src/main.jsx +9 -0
  79. package/packages/gui/src/stores/groove.js +247 -0
  80. package/packages/gui/src/theme.css +67 -0
  81. package/packages/gui/src/views/AgentTree.jsx +148 -0
  82. package/packages/gui/src/views/CommandCenter.jsx +620 -0
  83. package/packages/gui/src/views/JournalistFeed.jsx +149 -0
  84. package/packages/gui/vite.config.js +19 -0
@@ -0,0 +1,124 @@
1
+ // GROOVE — Token Tracker with Savings Calculator
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
5
+ import { resolve } from 'path';
6
+
7
+ // Estimated tokens wasted per cold-start without GROOVE context
8
+ const COLD_START_OVERHEAD = 2000;
9
+ // Estimated tokens wasted per file conflict (agent discovers, backs off, retries)
10
+ const CONFLICT_OVERHEAD = 500;
11
+
12
+ export class TokenTracker {
13
+ constructor(grooveDir) {
14
+ this.path = resolve(grooveDir, 'tokens.json');
15
+ this.usage = {};
16
+ this.sessionStart = Date.now();
17
+ this.rotationSavings = 0; // Tokens saved by rotation (context that would have degraded)
18
+ this.conflictsPrevented = 0;
19
+ this.coldStartsSkipped = 0;
20
+ this.load();
21
+ }
22
+
23
+ load() {
24
+ if (existsSync(this.path)) {
25
+ try {
26
+ const data = JSON.parse(readFileSync(this.path, 'utf8'));
27
+ this.usage = data.usage || data; // Handle both old and new format
28
+ this.rotationSavings = data.rotationSavings || 0;
29
+ this.conflictsPrevented = data.conflictsPrevented || 0;
30
+ this.coldStartsSkipped = data.coldStartsSkipped || 0;
31
+ } catch {
32
+ this.usage = {};
33
+ }
34
+ }
35
+ }
36
+
37
+ save() {
38
+ writeFileSync(this.path, JSON.stringify({
39
+ usage: this.usage,
40
+ rotationSavings: this.rotationSavings,
41
+ conflictsPrevented: this.conflictsPrevented,
42
+ coldStartsSkipped: this.coldStartsSkipped,
43
+ lastSaved: new Date().toISOString(),
44
+ }, null, 2));
45
+ }
46
+
47
+ record(agentId, tokens) {
48
+ if (!this.usage[agentId]) {
49
+ this.usage[agentId] = { total: 0, sessions: [] };
50
+ }
51
+ this.usage[agentId].total += tokens;
52
+ this.usage[agentId].sessions.push({
53
+ tokens,
54
+ timestamp: new Date().toISOString(),
55
+ });
56
+ this.save();
57
+ }
58
+
59
+ // Record that a rotation saved context tokens
60
+ recordRotation(agentId, tokensBefore) {
61
+ this.rotationSavings += Math.round(tokensBefore * 0.3); // ~30% of context was degraded
62
+ this.save();
63
+ }
64
+
65
+ // Record that a conflict was prevented (scope enforcement)
66
+ recordConflictPrevented() {
67
+ this.conflictsPrevented++;
68
+ this.save();
69
+ }
70
+
71
+ // Record that a cold-start was skipped (Journalist provided context)
72
+ recordColdStartSkipped() {
73
+ this.coldStartsSkipped++;
74
+ this.save();
75
+ }
76
+
77
+ getAgent(agentId) {
78
+ return this.usage[agentId] || { total: 0, sessions: [] };
79
+ }
80
+
81
+ getAll() {
82
+ return this.usage;
83
+ }
84
+
85
+ getTotal() {
86
+ return Object.values(this.usage).reduce((sum, a) => sum + a.total, 0);
87
+ }
88
+
89
+ // Generate a savings summary
90
+ getSummary() {
91
+ const totalTokens = this.getTotal();
92
+ const agentCount = Object.keys(this.usage).length;
93
+ const sessionDuration = Date.now() - this.sessionStart;
94
+
95
+ // Estimate what uncoordinated usage would have cost
96
+ const coldStartWaste = this.coldStartsSkipped * COLD_START_OVERHEAD;
97
+ const conflictWaste = this.conflictsPrevented * CONFLICT_OVERHEAD;
98
+ const totalSavings = this.rotationSavings + coldStartWaste + conflictWaste;
99
+
100
+ const estimatedWithout = totalTokens + totalSavings;
101
+ const savingsPct = estimatedWithout > 0
102
+ ? Math.round((totalSavings / estimatedWithout) * 100)
103
+ : 0;
104
+
105
+ return {
106
+ totalTokens,
107
+ agentCount,
108
+ sessionDurationMs: sessionDuration,
109
+ savings: {
110
+ total: totalSavings,
111
+ fromRotation: this.rotationSavings,
112
+ fromConflictPrevention: conflictWaste,
113
+ fromColdStartSkip: coldStartWaste,
114
+ percentage: savingsPct,
115
+ estimatedWithoutGroove: estimatedWithout,
116
+ },
117
+ perAgent: Object.entries(this.usage).map(([id, data]) => ({
118
+ agentId: id,
119
+ tokens: data.total,
120
+ sessions: data.sessions.length,
121
+ })),
122
+ };
123
+ }
124
+ }
@@ -0,0 +1,122 @@
1
+ // GROOVE — Input Validation
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ const NAME_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
5
+ const ROLE_PATTERN = /^[a-zA-Z0-9_-]{1,50}$/;
6
+ const PROVIDER_PATTERN = /^[a-zA-Z0-9_-]{1,30}$/;
7
+ const MAX_PROMPT_LENGTH = 50_000;
8
+ const MAX_SCOPE_PATTERNS = 20;
9
+ const MAX_SCOPE_LENGTH = 200;
10
+
11
+ export function validateAgentConfig(config) {
12
+ if (!config || typeof config !== 'object') {
13
+ throw new Error('Invalid agent config');
14
+ }
15
+
16
+ if (!config.role || typeof config.role !== 'string') {
17
+ throw new Error('Role is required');
18
+ }
19
+ if (!ROLE_PATTERN.test(config.role)) {
20
+ throw new Error('Invalid role: must be alphanumeric, dash, or underscore (max 50 chars)');
21
+ }
22
+
23
+ if (config.name !== undefined && config.name !== null) {
24
+ if (typeof config.name !== 'string' || !NAME_PATTERN.test(config.name)) {
25
+ throw new Error('Invalid name: must be alphanumeric, dash, or underscore (max 64 chars)');
26
+ }
27
+ }
28
+
29
+ if (config.provider !== undefined && config.provider !== null) {
30
+ if (typeof config.provider !== 'string' || !PROVIDER_PATTERN.test(config.provider)) {
31
+ throw new Error('Invalid provider name');
32
+ }
33
+ }
34
+
35
+ if (config.scope !== undefined && config.scope !== null) {
36
+ if (!Array.isArray(config.scope)) {
37
+ throw new Error('Scope must be an array');
38
+ }
39
+ if (config.scope.length > MAX_SCOPE_PATTERNS) {
40
+ throw new Error(`Too many scope patterns (max ${MAX_SCOPE_PATTERNS})`);
41
+ }
42
+ for (const pattern of config.scope) {
43
+ validateScopePattern(pattern);
44
+ }
45
+ }
46
+
47
+ if (config.prompt !== undefined && config.prompt !== null) {
48
+ if (typeof config.prompt !== 'string') {
49
+ throw new Error('Prompt must be a string');
50
+ }
51
+ if (config.prompt.length > MAX_PROMPT_LENGTH) {
52
+ throw new Error(`Prompt too long (max ${MAX_PROMPT_LENGTH} chars)`);
53
+ }
54
+ }
55
+
56
+ // Validate permission level
57
+ const validPermissions = ['auto', 'full'];
58
+ const permission = validPermissions.includes(config.permission) ? config.permission : 'full';
59
+
60
+ // Return sanitized config (only known fields)
61
+ return {
62
+ role: config.role,
63
+ name: config.name || undefined,
64
+ scope: config.scope || [],
65
+ prompt: config.prompt || '',
66
+ provider: config.provider || 'claude-code',
67
+ model: typeof config.model === 'string' ? config.model : null,
68
+ workingDir: typeof config.workingDir === 'string' ? config.workingDir : undefined,
69
+ permission,
70
+ };
71
+ }
72
+
73
+ export function validateScopePattern(pattern) {
74
+ if (typeof pattern !== 'string') {
75
+ throw new Error('Scope pattern must be a string');
76
+ }
77
+ if (pattern.length > MAX_SCOPE_LENGTH) {
78
+ throw new Error(`Scope pattern too long (max ${MAX_SCOPE_LENGTH} chars)`);
79
+ }
80
+ // No absolute paths
81
+ if (pattern.startsWith('/')) {
82
+ throw new Error('Scope patterns cannot be absolute paths');
83
+ }
84
+ // No path traversal
85
+ if (pattern.includes('..')) {
86
+ throw new Error('Scope patterns cannot contain path traversal (..)');
87
+ }
88
+ // No null bytes
89
+ if (pattern.includes('\0')) {
90
+ throw new Error('Scope pattern contains invalid characters');
91
+ }
92
+ }
93
+
94
+ export function validateTeamName(name) {
95
+ if (!name || typeof name !== 'string') {
96
+ throw new Error('Team name is required');
97
+ }
98
+ if (name.length > 100) {
99
+ throw new Error('Team name too long (max 100 chars)');
100
+ }
101
+ // Allow spaces and special chars in display name, sanitize for filesystem separately
102
+ }
103
+
104
+ export function sanitizeForFilename(name) {
105
+ const sanitized = name
106
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
107
+ .replace(/_+/g, '_')
108
+ .toLowerCase()
109
+ .slice(0, 50);
110
+
111
+ if (!sanitized || /^_+$/.test(sanitized)) {
112
+ throw new Error('Team name must contain alphanumeric characters');
113
+ }
114
+ return sanitized;
115
+ }
116
+
117
+ export function escapeMd(text) {
118
+ if (!text) return '';
119
+ // Escape markdown special chars that could break table rendering or inject formatting.
120
+ // Keep hyphens and dots since they're common in agent names and safe in table cells.
121
+ return String(text).replace(/[|\\`*_{}[\]()#+!]/g, '\\$&');
122
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "api-builder",
3
+ "description": "Backend + Testing agents for API development",
4
+ "agents": [
5
+ {
6
+ "role": "backend",
7
+ "scope": ["src/**", "lib/**"],
8
+ "provider": "claude-code",
9
+ "prompt": "You are the backend agent. Build the API endpoints, business logic, and data layer."
10
+ },
11
+ {
12
+ "role": "testing",
13
+ "scope": ["tests/**", "test/**", "**/*.test.*", "**/*.spec.*"],
14
+ "provider": "claude-code",
15
+ "prompt": "You are the testing agent. Write comprehensive tests for the API. Monitor what the backend agent builds and ensure test coverage."
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "fullstack",
3
+ "description": "Backend + Frontend agents with scoped file ownership",
4
+ "agents": [
5
+ {
6
+ "role": "backend",
7
+ "scope": ["src/api/**", "src/server/**", "src/lib/**", "src/db/**"],
8
+ "provider": "claude-code",
9
+ "prompt": "You are the backend agent. Build and maintain the server-side code, APIs, database logic, and server utilities."
10
+ },
11
+ {
12
+ "role": "frontend",
13
+ "scope": ["src/components/**", "src/views/**", "src/pages/**", "src/styles/**", "src/hooks/**"],
14
+ "provider": "claude-code",
15
+ "prompt": "You are the frontend agent. Build and maintain the UI components, views, pages, styles, and client-side logic."
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "monorepo",
3
+ "description": "3-agent setup for monorepo projects: backend, frontend, and shared/infra",
4
+ "agents": [
5
+ {
6
+ "role": "backend",
7
+ "scope": ["packages/api/**", "packages/server/**"],
8
+ "provider": "claude-code",
9
+ "prompt": "You are the backend agent. Build and maintain the API and server packages."
10
+ },
11
+ {
12
+ "role": "frontend",
13
+ "scope": ["packages/web/**", "packages/ui/**"],
14
+ "provider": "claude-code",
15
+ "prompt": "You are the frontend agent. Build and maintain the web app and UI component packages."
16
+ },
17
+ {
18
+ "role": "infra",
19
+ "scope": ["packages/shared/**", "packages/config/**", "docker*", ".github/**", "infra/**"],
20
+ "provider": "claude-code",
21
+ "prompt": "You are the infrastructure agent. Manage shared packages, configuration, CI/CD, and deployment."
22
+ }
23
+ ]
24
+ }