groove-dev 0.27.216 → 0.27.220

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.
@@ -6,7 +6,7 @@
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-DDdFdSWx.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BUmxCPjv.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BYKpdS2W.js">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.216",
3
+ "version": "0.27.220",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.216",
3
+ "version": "0.27.220",
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.216",
3
+ "version": "0.27.220",
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.216",
3
+ "version": "0.27.220",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -39,10 +39,16 @@ export class ClaudeCodeProvider extends Provider {
39
39
  static authType = 'subscription';
40
40
  static managesOwnContext = true; // Claude Code compacts context internally (~25-37% → 2-8%)
41
41
  static models = [
42
- { id: 'claude-opus-5', name: 'Claude Opus 5', tier: 'heavy', contextWindow: 1_000_000 },
42
+ // Order matters: the Router defaults to models[0] and tier lookups take the
43
+ // FIRST model of a tier. Opus 5 stays first so auto-routing doesn't silently
44
+ // move heavy agents onto the Fable price tier (2× Opus per token) — Fable is
45
+ // an explicit user choice.
46
+ { id: 'claude-opus-5-5', name: 'Claude Opus 5.5', tier: 'heavy', contextWindow: 1_000_000 },
47
+ { id: 'claude-fable-5-1', name: 'Claude Fable 5.1', tier: 'heavy', contextWindow: 1_000_000 },
43
48
  { id: 'claude-fable-5', name: 'Claude Fable 5', tier: 'heavy', contextWindow: 1_000_000 },
44
49
  { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', tier: 'heavy', contextWindow: 1_000_000 },
45
50
  { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', tier: 'heavy', contextWindow: 1_000_000 },
51
+ { id: 'claude-sonnet-5', name: 'Claude Sonnet 5', tier: 'medium', contextWindow: 1_000_000 },
46
52
  { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', tier: 'medium', contextWindow: 200_000 },
47
53
  { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', tier: 'light', contextWindow: 200_000 },
48
54
  ];
@@ -4,6 +4,7 @@
4
4
  import { spawn, execFileSync } from 'child_process';
5
5
  import { randomUUID } from 'crypto';
6
6
  import { existsSync } from 'fs';
7
+ import { StringDecoder } from 'string_decoder';
7
8
 
8
9
  // Python helper that creates a real PTY and relays I/O through stdin/stdout pipes.
9
10
  // The shell sees a genuine TTY — prompts, colors, line editing, tab completion all work.
@@ -64,12 +65,22 @@ try:
64
65
  bel_pos = stdin_buf.find(ESC_END)
65
66
  if bel_pos == -1:
66
67
  break
67
- params = stdin_buf[4:bel_pos].decode().split(';')
68
- if len(params) == 2:
69
- r, c = int(params[0]), int(params[1])
70
- fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack('HHHH', r, c, 0, 0))
71
- os.kill(pid, signal.SIGWINCH)
68
+ # Only OUR resize command lives on this channel — no real terminal
69
+ # sends OSC 7 as keyboard input. A malformed one used to raise
70
+ # inside this loop, escape it, and take the whole shell down; now
71
+ # it is simply dropped (forwarding it would feed the shell's line
72
+ # editor a garbage key chord).
73
+ raw = stdin_buf[:bel_pos+1]
72
74
  stdin_buf = stdin_buf[bel_pos+1:]
75
+ try:
76
+ params = raw[4:-1].decode().split(';')
77
+ if len(params) == 2:
78
+ r, c = int(params[0]), int(params[1])
79
+ if 0 < r < 1000 and 0 < c < 1000:
80
+ fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack('HHHH', r, c, 0, 0))
81
+ os.kill(pid, signal.SIGWINCH)
82
+ except (ValueError, UnicodeDecodeError, OSError):
83
+ pass
73
84
  if master in rlist:
74
85
  try:
75
86
  data = os.read(master, 4096)
@@ -128,16 +139,23 @@ export class TerminalManager {
128
139
  const session = { proc, ws, id, label: options.label || '' };
129
140
  this.sessions.set(id, session);
130
141
 
142
+ // The PTY relay hands us arbitrary 4096-byte slices, and a multi-byte UTF-8
143
+ // character (box drawing, emoji — TUI apps are full of them) can straddle a
144
+ // slice. Buffer.toString() on each slice independently turns every such
145
+ // character into U+FFFD garbage; a StringDecoder carries the partial byte
146
+ // sequence across chunks.
147
+ const outDecoder = new StringDecoder('utf8');
148
+ const errDecoder = new StringDecoder('utf8');
131
149
  proc.stdout.on('data', (data) => {
132
- if (ws.readyState === 1) {
133
- ws.send(JSON.stringify({ type: 'terminal:output', id, data: data.toString('utf8') }));
134
- }
150
+ if (ws.readyState !== 1) return;
151
+ const text = outDecoder.write(data);
152
+ if (text) ws.send(JSON.stringify({ type: 'terminal:output', id, data: text }));
135
153
  });
136
154
 
137
155
  proc.stderr.on('data', (data) => {
138
- if (ws.readyState === 1) {
139
- ws.send(JSON.stringify({ type: 'terminal:output', id, data: data.toString('utf8') }));
140
- }
156
+ if (ws.readyState !== 1) return;
157
+ const text = errDecoder.write(data);
158
+ if (text) ws.send(JSON.stringify({ type: 'terminal:output', id, data: text }));
141
159
  });
142
160
 
143
161
  proc.on('exit', (code) => {
@@ -15,6 +15,20 @@ function getLocalVersion() {
15
15
  } catch { return '0.0.0'; }
16
16
  }
17
17
 
18
+ // True only when `a` is strictly older than `b` (numeric x.y.z; any suffix such
19
+ // as "-local" is ignored). Auto-upgrade must never move a remote BACKWARDS: a
20
+ // box running a newer local build (promote-local.sh) than npm's latest would
21
+ // otherwise be "upgraded" down to the published release on every connect.
22
+ export function isOlderVersion(a, b) {
23
+ const parse = (v) => String(v || '').split('-')[0].split('.').map((n) => parseInt(n, 10) || 0);
24
+ const pa = parse(a), pb = parse(b);
25
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
26
+ const d = (pa[i] || 0) - (pb[i] || 0);
27
+ if (d !== 0) return d < 0;
28
+ }
29
+ return false;
30
+ }
31
+
18
32
  const REMOTE_PORT = 31415;
19
33
 
20
34
  // Every remote `groove start` must run from the directory of the daemon's
@@ -665,13 +679,13 @@ export class TunnelManager {
665
679
  }).trim();
666
680
  } catch { return; }
667
681
 
668
- if (!npmVer || npmVer === remoteVer) {
682
+ if (!npmVer || !isOlderVersion(remoteVer, npmVer)) {
669
683
  const localVer = getLocalVersion();
670
684
  this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: localVer, remoteVersion: remoteVer, match: remoteVer === localVer } });
671
685
  return;
672
686
  }
673
687
 
674
- // Remote is behind npm — upgrade
688
+ // Remote is strictly behind npm — upgrade
675
689
  this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'upgrading', from: remoteVer, to: npmVer } });
676
690
 
677
691
  const target = `${config.user}@${config.host}`;