groove-dev 0.27.217 → 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.
- package/.promote-remotes +1 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +2 -1
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +29 -11
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +16 -2
- package/node_modules/@groove-dev/daemon/test/claude-code-provider.test.js +4 -4
- package/node_modules/@groove-dev/daemon/test/terminal-pty.test.js +80 -0
- package/node_modules/@groove-dev/daemon/test/tunnel-manager.test.js +11 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BUmxCPjv.js +1055 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/providers/claude-code.js +2 -1
- package/packages/daemon/src/terminal-pty.js +29 -11
- package/packages/daemon/src/tunnel-manager.js +16 -2
- package/packages/gui/dist/assets/index-BUmxCPjv.js +1055 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DDdFdSWx.js +0 -1099
- package/packages/gui/dist/assets/index-DDdFdSWx.js +0 -1099
package/.promote-remotes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
axom@10.0.0.205 # DGX Spark (edgexpert-aaa6) — "axom" is just the login user on that box
|
|
@@ -43,11 +43,12 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
43
43
|
// FIRST model of a tier. Opus 5 stays first so auto-routing doesn't silently
|
|
44
44
|
// move heavy agents onto the Fable price tier (2× Opus per token) — Fable is
|
|
45
45
|
// an explicit user choice.
|
|
46
|
-
{ id: 'claude-opus-5', name: 'Claude Opus 5', tier: 'heavy', contextWindow: 1_000_000 },
|
|
46
|
+
{ id: 'claude-opus-5-5', name: 'Claude Opus 5.5', tier: 'heavy', contextWindow: 1_000_000 },
|
|
47
47
|
{ id: 'claude-fable-5-1', name: 'Claude Fable 5.1', tier: 'heavy', contextWindow: 1_000_000 },
|
|
48
48
|
{ id: 'claude-fable-5', name: 'Claude Fable 5', tier: 'heavy', contextWindow: 1_000_000 },
|
|
49
49
|
{ id: 'claude-opus-4-8', name: 'Claude Opus 4.8', tier: 'heavy', contextWindow: 1_000_000 },
|
|
50
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 },
|
|
51
52
|
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', tier: 'medium', contextWindow: 200_000 },
|
|
52
53
|
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', tier: 'light', contextWindow: 200_000 },
|
|
53
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
133
|
-
|
|
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
|
|
139
|
-
|
|
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
|
|
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}`;
|
|
@@ -125,9 +125,9 @@ describe('ClaudeCodeProvider models', () => {
|
|
|
125
125
|
it('offers Fable 5', () => {
|
|
126
126
|
assert.ok(ClaudeCodeProvider.models.some((m) => m.id === 'claude-fable-5'));
|
|
127
127
|
assert.ok(ClaudeCodeProvider.models.some((m) => m.id === 'claude-fable-5-1'));
|
|
128
|
-
// Routing guard: Opus
|
|
129
|
-
// putting a Fable model first would silently route heavy agents onto
|
|
130
|
-
// priced at twice Opus.
|
|
131
|
-
assert.equal(ClaudeCodeProvider.models[0].id, 'claude-opus-5');
|
|
128
|
+
// Routing guard: an Opus model must stay models[0] — the Router defaults to
|
|
129
|
+
// it and putting a Fable model first would silently route heavy agents onto
|
|
130
|
+
// a tier priced at twice Opus.
|
|
131
|
+
assert.equal(ClaudeCodeProvider.models[0].id, 'claude-opus-5-5');
|
|
132
132
|
});
|
|
133
133
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// GROOVE — Terminal PTY relay tests
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
//
|
|
4
|
+
// Spawns the real Python PTY helper + a shell. Guards the relay against the
|
|
5
|
+
// failures that made TUI apps (Textual etc.) render garbage or die:
|
|
6
|
+
// - multi-byte UTF-8 split across 4096-byte relay chunks → U+FFFD garbage
|
|
7
|
+
// - a malformed in-band resize sequence killing the whole relay loop
|
|
8
|
+
|
|
9
|
+
import { describe, it, afterEach } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { TerminalManager } from '../src/terminal-pty.js';
|
|
12
|
+
|
|
13
|
+
function fakeWs() {
|
|
14
|
+
const ws = { readyState: 1, out: [], exit: null };
|
|
15
|
+
ws.send = (raw) => {
|
|
16
|
+
const m = JSON.parse(raw);
|
|
17
|
+
if (m.type === 'terminal:output') ws.out.push(m.data);
|
|
18
|
+
if (m.type === 'terminal:exit') ws.exit = m.code;
|
|
19
|
+
};
|
|
20
|
+
return ws;
|
|
21
|
+
}
|
|
22
|
+
const settle = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
23
|
+
async function waitFor(pred, ms = 20000) {
|
|
24
|
+
const end = Date.now() + ms;
|
|
25
|
+
while (Date.now() < end) { if (pred()) return true; await settle(100); }
|
|
26
|
+
return pred();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('TerminalManager (real PTY helper)', () => {
|
|
30
|
+
let mgr;
|
|
31
|
+
afterEach(() => { mgr?.killAll(); });
|
|
32
|
+
|
|
33
|
+
it('relays multi-byte UTF-8 intact across chunk boundaries', async () => {
|
|
34
|
+
mgr = new TerminalManager({ projectDir: '/tmp' });
|
|
35
|
+
const ws = fakeWs();
|
|
36
|
+
const id = mgr.spawn(ws, { cols: 100, rows: 30, cwd: '/tmp' });
|
|
37
|
+
assert.ok(id);
|
|
38
|
+
await waitFor(() => ws.out.join('').length > 0);
|
|
39
|
+
|
|
40
|
+
// 3000 × '★' (3 bytes each) = 9000 bytes: guaranteed to straddle the
|
|
41
|
+
// helper's 4096-byte reads several times. The marker is assembled at
|
|
42
|
+
// runtime from two halves so the shell's ECHO of the typed command never
|
|
43
|
+
// contains it — only the real output does. (Counting echoed markers made
|
|
44
|
+
// this race on shells that redraw the input line.)
|
|
45
|
+
const marker = 'UTF8TEST';
|
|
46
|
+
mgr.write(id, `M="UTF8""TEST"; printf "$M"; python3 -c "print('\\u2605'*3000, end='')"; printf "$M\\n"\r`);
|
|
47
|
+
const ok = await waitFor(() => {
|
|
48
|
+
const all = ws.out.join('');
|
|
49
|
+
const first = all.indexOf(marker);
|
|
50
|
+
return first !== -1 && all.indexOf(marker, first + marker.length) !== -1;
|
|
51
|
+
});
|
|
52
|
+
assert.ok(ok, 'command output arrived');
|
|
53
|
+
|
|
54
|
+
const all = ws.out.join('');
|
|
55
|
+
const body = all.slice(all.indexOf(marker) + marker.length, all.lastIndexOf(marker));
|
|
56
|
+
assert.equal((body.match(/�/g) || []).length, 0, 'no replacement characters');
|
|
57
|
+
assert.equal((body.match(/★/g) || []).length, 3000, 'every star survived the relay');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('resizes the PTY in-band and survives a malformed resize sequence', async () => {
|
|
61
|
+
mgr = new TerminalManager({ projectDir: '/tmp' });
|
|
62
|
+
const ws = fakeWs();
|
|
63
|
+
const id = mgr.spawn(ws, { cols: 80, rows: 24, cwd: '/tmp' });
|
|
64
|
+
await waitFor(() => ws.out.join('').length > 0);
|
|
65
|
+
|
|
66
|
+
mgr.resize(id, 40, 132);
|
|
67
|
+
await settle(300);
|
|
68
|
+
mgr.write(id, 'stty size\r');
|
|
69
|
+
assert.ok(await waitFor(() => /40 132/.test(ws.out.join(''))), 'shell sees the new size');
|
|
70
|
+
|
|
71
|
+
// Garbage that shares our OSC 7 prefix — previously raised inside the relay
|
|
72
|
+
// loop, escaped it, and took the shell down with it.
|
|
73
|
+
mgr.write(id, '\x1b]7;not;numbers;at;all\x07');
|
|
74
|
+
mgr.write(id, '\x1b]7;file://host/tmp\x07');
|
|
75
|
+
await settle(300);
|
|
76
|
+
mgr.write(id, 'echo STILL_ALIVE_$((6*7))\r');
|
|
77
|
+
assert.ok(await waitFor(() => ws.out.join('').includes('STILL_ALIVE_42')), 'relay survived');
|
|
78
|
+
assert.equal(ws.exit, null, 'session did not exit');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -392,3 +392,14 @@ describe('resolveBestAddress — multi-homed host selection', () => {
|
|
|
392
392
|
assert.equal(await resolveBestAddress('localhost', deadPort, 500), 'localhost');
|
|
393
393
|
});
|
|
394
394
|
});
|
|
395
|
+
|
|
396
|
+
describe('isOlderVersion (auto-upgrade never downgrades)', () => {
|
|
397
|
+
it('only reports strictly older versions', async () => {
|
|
398
|
+
const { isOlderVersion } = await import('../src/tunnel-manager.js');
|
|
399
|
+
assert.equal(isOlderVersion('0.27.216', '0.27.217'), true);
|
|
400
|
+
assert.equal(isOlderVersion('0.27.9', '0.27.10'), true);
|
|
401
|
+
assert.equal(isOlderVersion('0.27.219', '0.27.217'), false); // local build ahead of npm
|
|
402
|
+
assert.equal(isOlderVersion('0.27.217', '0.27.217'), false);
|
|
403
|
+
assert.equal(isOlderVersion('0.27.219-local', '0.27.217'), false);
|
|
404
|
+
});
|
|
405
|
+
});
|