groove-dev 0.11.0 → 0.12.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 (39) hide show
  1. package/node_modules/@groove-dev/cli/bin/groove.js +32 -0
  2. package/node_modules/@groove-dev/cli/src/commands/audit.js +60 -0
  3. package/node_modules/@groove-dev/cli/src/commands/connect.js +279 -0
  4. package/node_modules/@groove-dev/cli/src/commands/disconnect.js +91 -0
  5. package/node_modules/@groove-dev/cli/src/commands/federation.js +84 -0
  6. package/node_modules/@groove-dev/cli/src/commands/start.js +7 -2
  7. package/node_modules/@groove-dev/cli/src/commands/status.js +4 -1
  8. package/node_modules/@groove-dev/daemon/src/api.js +106 -2
  9. package/node_modules/@groove-dev/daemon/src/audit.js +65 -0
  10. package/node_modules/@groove-dev/daemon/src/federation.js +352 -0
  11. package/node_modules/@groove-dev/daemon/src/firstrun.js +27 -2
  12. package/node_modules/@groove-dev/daemon/src/index.js +59 -6
  13. package/node_modules/@groove-dev/gui/dist/assets/{index-BqZnnVJF.js → index-B49YqEXS.js} +17 -17
  14. package/node_modules/@groove-dev/gui/dist/assets/{index-CPzm9ZE9.css → index-Gfb8Zxy9.css} +1 -1
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/src/App.jsx +24 -1
  17. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +1 -1
  18. package/node_modules/@groove-dev/gui/src/stores/groove.js +19 -2
  19. package/node_modules/@groove-dev/gui/src/theme.css +2 -2
  20. package/package.json +1 -1
  21. package/packages/cli/bin/groove.js +32 -0
  22. package/packages/cli/src/commands/audit.js +60 -0
  23. package/packages/cli/src/commands/connect.js +279 -0
  24. package/packages/cli/src/commands/disconnect.js +91 -0
  25. package/packages/cli/src/commands/federation.js +84 -0
  26. package/packages/cli/src/commands/start.js +7 -2
  27. package/packages/cli/src/commands/status.js +4 -1
  28. package/packages/daemon/src/api.js +106 -2
  29. package/packages/daemon/src/audit.js +65 -0
  30. package/packages/daemon/src/federation.js +352 -0
  31. package/packages/daemon/src/firstrun.js +27 -2
  32. package/packages/daemon/src/index.js +59 -6
  33. package/packages/gui/dist/assets/{index-BqZnnVJF.js → index-B49YqEXS.js} +17 -17
  34. package/packages/gui/dist/assets/{index-CPzm9ZE9.css → index-Gfb8Zxy9.css} +1 -1
  35. package/packages/gui/dist/index.html +2 -2
  36. package/packages/gui/src/App.jsx +24 -1
  37. package/packages/gui/src/components/SpawnPanel.jsx +1 -1
  38. package/packages/gui/src/stores/groove.js +19 -2
  39. package/packages/gui/src/theme.css +2 -2
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { createServer as createHttpServer } from 'http';
5
5
  import { createServer as createNetServer } from 'net';
6
+ import { execFileSync } from 'child_process';
6
7
  import { resolve } from 'path';
7
8
  import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
8
9
  import express from 'express';
@@ -24,15 +25,55 @@ import { TaskClassifier } from './classifier.js';
24
25
  import { ModelRouter } from './router.js';
25
26
  import { ProjectManager } from './pm.js';
26
27
  import { CodebaseIndexer } from './indexer.js';
28
+ import { AuditLogger } from './audit.js';
29
+ import { Federation } from './federation.js';
27
30
  import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
28
31
 
29
32
  const DEFAULT_PORT = 31415;
33
+ const DEFAULT_HOST = '127.0.0.1';
30
34
 
31
35
  export { loadConfig, saveConfig } from './firstrun.js';
32
36
 
37
+ /**
38
+ * Resolve the --host value to a bind address.
39
+ * - 'tailscale' → auto-detect via `tailscale ip -4`
40
+ * - '0.0.0.0' / '::' → rejected (security policy)
41
+ * - anything else → used as-is
42
+ */
43
+ function resolveHost(host) {
44
+ if (!host || host === 'localhost') return DEFAULT_HOST;
45
+
46
+ // Block direct internet exposure
47
+ if (host === '0.0.0.0' || host === '::') {
48
+ console.error('\n Direct internet exposure not supported.');
49
+ console.error(' Use `groove connect` (SSH tunnel) or `--host tailscale` instead.\n');
50
+ process.exit(1);
51
+ }
52
+
53
+ // Auto-detect Tailscale IP
54
+ if (host === 'tailscale') {
55
+ try {
56
+ const ip = execFileSync('tailscale', ['ip', '-4'], {
57
+ encoding: 'utf8',
58
+ timeout: 5000,
59
+ }).trim().split('\n')[0];
60
+ if (!ip) throw new Error('empty response');
61
+ return ip;
62
+ } catch (err) {
63
+ console.error('\n Could not detect Tailscale IP.');
64
+ console.error(' Make sure Tailscale is installed and running: `tailscale status`');
65
+ console.error(` Error: ${err.message}\n`);
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ return host;
71
+ }
72
+
33
73
  export class Daemon {
34
74
  constructor(options = {}) {
35
75
  this.port = options.port !== undefined ? options.port : (parseInt(process.env.GROOVE_PORT, 10) || DEFAULT_PORT);
76
+ this.host = resolveHost(options.host);
36
77
  this.projectDir = options.projectDir || process.cwd();
37
78
  this.grooveDir = options.grooveDir || resolve(this.projectDir, '.groove');
38
79
  this.pidFile = resolve(this.grooveDir, 'daemon.pid');
@@ -71,6 +112,8 @@ export class Daemon {
71
112
  this.router = new ModelRouter(this);
72
113
  this.pm = new ProjectManager(this);
73
114
  this.indexer = new CodebaseIndexer(this);
115
+ this.audit = new AuditLogger(this.grooveDir);
116
+ this.federation = new Federation(this);
74
117
 
75
118
  // HTTP + WebSocket server
76
119
  this.app = express();
@@ -80,13 +123,17 @@ export class Daemon {
80
123
  maxPayload: 1024 * 1024, // 1MB max message
81
124
  verifyClient: ({ req }) => {
82
125
  const origin = req.headers.origin;
83
- // Allow: no origin (CLI/native clients), localhost origins
126
+ // Allow: no origin (CLI/native clients), localhost origins, bound host
84
127
  if (!origin) return true;
85
128
  const allowed = [
86
129
  `http://localhost:${this.port}`,
87
130
  `http://127.0.0.1:${this.port}`,
88
131
  'http://localhost:3142',
89
132
  ];
133
+ // Allow the bound interface (for Tailscale/LAN access)
134
+ if (this.host !== DEFAULT_HOST) {
135
+ allowed.push(`http://${this.host}:${this.port}`);
136
+ }
90
137
  return allowed.includes(origin);
91
138
  },
92
139
  });
@@ -143,11 +190,12 @@ export class Daemon {
143
190
  }
144
191
 
145
192
  // Auto-find an open port if the default is taken
193
+ const bindHost = this.host;
146
194
  const checkPort = (port) => new Promise((res) => {
147
195
  const tester = createNetServer();
148
196
  tester.once('error', () => res(false));
149
197
  tester.once('listening', () => { tester.close(); res(true); });
150
- tester.listen(port, '127.0.0.1');
198
+ tester.listen(port, bindHost);
151
199
  }).catch(() => false);
152
200
 
153
201
  if (!(await checkPort(this.port))) {
@@ -172,12 +220,13 @@ export class Daemon {
172
220
  this.registry.restore(this.state.get('agents') || []);
173
221
 
174
222
  return new Promise((resolvePromise) => {
175
- this.server.listen(this.port, '127.0.0.1', () => {
223
+ this.server.listen(this.port, this.host, () => {
176
224
  writeFileSync(this.pidFile, String(process.pid));
177
- // Write actual port so CLI can find us (supports auto-port rotation)
225
+ // Write actual port and host so CLI can find us
178
226
  writeFileSync(resolve(this.grooveDir, 'daemon.port'), String(this.port));
227
+ writeFileSync(resolve(this.grooveDir, 'daemon.host'), this.host);
179
228
 
180
- printWelcome(this.port);
229
+ printWelcome(this.port, this.host);
181
230
 
182
231
  // Start background services
183
232
  this.journalist.start();
@@ -203,10 +252,14 @@ export class Daemon {
203
252
  // Kill all agent processes
204
253
  await this.processes.killAll();
205
254
 
206
- // Clean up PID file
255
+ // Clean up PID and host files
207
256
  if (existsSync(this.pidFile)) {
208
257
  unlinkSync(this.pidFile);
209
258
  }
259
+ const hostFile = resolve(this.grooveDir, 'daemon.host');
260
+ if (existsSync(hostFile)) {
261
+ unlinkSync(hostFile);
262
+ }
210
263
 
211
264
  // Clean up generated files
212
265
  const registryPath = resolve(this.projectDir, 'AGENTS_REGISTRY.md');