claude-remote-cli 2.14.3 → 2.15.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.
- package/dist/frontend/assets/index-BM9I3SdI.js +47 -0
- package/dist/frontend/index.html +1 -1
- package/dist/frontend/sw.js +39 -1
- package/dist/server/config.js +1 -0
- package/dist/server/index.js +44 -1
- package/dist/server/push.js +60 -0
- package/dist/server/sessions.js +6 -6
- package/package.json +3 -1
- package/dist/frontend/assets/index-D390PVqN.js +0 -47
package/dist/frontend/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
12
12
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
13
13
|
<meta name="theme-color" content="#1a1a1a" />
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-BM9I3SdI.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-t15zfL9Q.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
package/dist/frontend/sw.js
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Service worker for PWA install support and push notifications.
|
|
2
2
|
// Does not cache — all requests pass through to the network.
|
|
3
3
|
self.addEventListener('fetch', function (event) {
|
|
4
4
|
event.respondWith(fetch(event.request));
|
|
5
5
|
});
|
|
6
|
+
|
|
7
|
+
// Push notification handler
|
|
8
|
+
self.addEventListener('push', function (event) {
|
|
9
|
+
var data = {};
|
|
10
|
+
try { data = event.data.json(); } catch (e) { /* ignore parse errors */ }
|
|
11
|
+
event.waitUntil(
|
|
12
|
+
self.registration.showNotification(data.displayName || 'Claude Remote CLI', {
|
|
13
|
+
body: 'Session needs your input',
|
|
14
|
+
tag: 'session-' + (data.sessionId || ''),
|
|
15
|
+
data: { sessionId: data.sessionId, sessionType: data.sessionType },
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Notification click handler — focus existing tab or open new one
|
|
21
|
+
self.addEventListener('notificationclick', function (event) {
|
|
22
|
+
event.notification.close();
|
|
23
|
+
var notifData = event.notification.data || {};
|
|
24
|
+
var sessionId = notifData.sessionId;
|
|
25
|
+
var sessionType = notifData.sessionType;
|
|
26
|
+
if (!sessionId) return;
|
|
27
|
+
var tabMap = { repo: 'repos', worktree: 'worktrees' };
|
|
28
|
+
var tab = tabMap[sessionType] || 'repos';
|
|
29
|
+
var url = '/?session=' + sessionId + '&tab=' + tab;
|
|
30
|
+
|
|
31
|
+
event.waitUntil(
|
|
32
|
+
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (clientList) {
|
|
33
|
+
for (var i = 0; i < clientList.length; i++) {
|
|
34
|
+
var client = clientList[i];
|
|
35
|
+
if (client.url.indexOf(self.location.origin) !== -1) {
|
|
36
|
+
client.postMessage({ type: 'notification-click', sessionId: sessionId, sessionType: sessionType });
|
|
37
|
+
return client.focus();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return clients.openWindow(url);
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
});
|
package/dist/server/config.js
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListP
|
|
|
17
17
|
import { isInstalled as serviceIsInstalled } from './service.js';
|
|
18
18
|
import { extensionForMime, setClipboardImage } from './clipboard.js';
|
|
19
19
|
import { listBranches } from './git.js';
|
|
20
|
+
import * as push from './push.js';
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
22
|
const __dirname = path.dirname(__filename);
|
|
22
23
|
const execFileAsync = promisify(execFile);
|
|
@@ -155,6 +156,7 @@ async function main() {
|
|
|
155
156
|
config.port = parseInt(process.env.CLAUDE_REMOTE_PORT, 10);
|
|
156
157
|
if (process.env.CLAUDE_REMOTE_HOST)
|
|
157
158
|
config.host = process.env.CLAUDE_REMOTE_HOST;
|
|
159
|
+
push.ensureVapidKeys(config, CONFIG_PATH, saveConfig);
|
|
158
160
|
if (!config.pinHash) {
|
|
159
161
|
const pin = await promptPin('Set up a PIN for claude-remote-cli:');
|
|
160
162
|
config.pinHash = await auth.hashPin(pin);
|
|
@@ -221,6 +223,15 @@ async function main() {
|
|
|
221
223
|
watcher.rebuild(config.rootDirs || []);
|
|
222
224
|
const server = http.createServer(app);
|
|
223
225
|
const { broadcastEvent } = setupWebSocket(server, authenticatedTokens, watcher);
|
|
226
|
+
// Push notifications on session idle
|
|
227
|
+
sessions.onIdleChange((sessionId, idle) => {
|
|
228
|
+
if (idle) {
|
|
229
|
+
const session = sessions.get(sessionId);
|
|
230
|
+
if (session && session.type !== 'terminal') {
|
|
231
|
+
push.notifySessionIdle(sessionId, session);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
224
235
|
// POST /auth
|
|
225
236
|
app.post('/auth', async (req, res) => {
|
|
226
237
|
const ip = (req.ip || req.connection.remoteAddress);
|
|
@@ -535,6 +546,36 @@ async function main() {
|
|
|
535
546
|
boolConfigEndpoints('launchInTmux', false, async () => {
|
|
536
547
|
await execFileAsync('tmux', ['-V']);
|
|
537
548
|
});
|
|
549
|
+
boolConfigEndpoints('defaultNotifications', true);
|
|
550
|
+
// GET /push/vapid-key
|
|
551
|
+
app.get('/push/vapid-key', requireAuth, (_req, res) => {
|
|
552
|
+
const key = push.getVapidPublicKey();
|
|
553
|
+
if (!key) {
|
|
554
|
+
res.status(501).json({ error: 'Push not available' });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
res.json({ vapidPublicKey: key });
|
|
558
|
+
});
|
|
559
|
+
// POST /push/subscribe
|
|
560
|
+
app.post('/push/subscribe', requireAuth, (req, res) => {
|
|
561
|
+
const { subscription, sessionIds } = req.body;
|
|
562
|
+
if (!subscription?.endpoint) {
|
|
563
|
+
res.status(400).json({ error: 'subscription required' });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
push.subscribe(subscription, sessionIds || []);
|
|
567
|
+
res.json({ ok: true });
|
|
568
|
+
});
|
|
569
|
+
// POST /push/unsubscribe
|
|
570
|
+
app.post('/push/unsubscribe', requireAuth, (req, res) => {
|
|
571
|
+
const { endpoint } = req.body;
|
|
572
|
+
if (!endpoint) {
|
|
573
|
+
res.status(400).json({ error: 'endpoint required' });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
push.unsubscribe(endpoint);
|
|
577
|
+
res.json({ ok: true });
|
|
578
|
+
});
|
|
538
579
|
// DELETE /worktrees — remove a worktree, prune, and delete its branch
|
|
539
580
|
app.delete('/worktrees', requireAuth, async (req, res) => {
|
|
540
581
|
const { worktreePath, repoPath } = req.body;
|
|
@@ -827,8 +868,10 @@ async function main() {
|
|
|
827
868
|
});
|
|
828
869
|
// DELETE /sessions/:id
|
|
829
870
|
app.delete('/sessions/:id', requireAuth, (req, res) => {
|
|
871
|
+
const id = req.params['id'];
|
|
830
872
|
try {
|
|
831
|
-
sessions.kill(
|
|
873
|
+
sessions.kill(id);
|
|
874
|
+
push.removeSession(id);
|
|
832
875
|
res.json({ ok: true });
|
|
833
876
|
}
|
|
834
877
|
catch (_) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import webpush from 'web-push';
|
|
2
|
+
let vapidPublicKey = null;
|
|
3
|
+
const subscriptions = new Map();
|
|
4
|
+
export function ensureVapidKeys(config, configPath, save) {
|
|
5
|
+
if (config.vapidPublicKey && config.vapidPrivateKey) {
|
|
6
|
+
vapidPublicKey = config.vapidPublicKey;
|
|
7
|
+
webpush.setVapidDetails('mailto:noreply@claude-remote-cli.local', config.vapidPublicKey, config.vapidPrivateKey);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const keys = webpush.generateVAPIDKeys();
|
|
12
|
+
config.vapidPublicKey = keys.publicKey;
|
|
13
|
+
config.vapidPrivateKey = keys.privateKey;
|
|
14
|
+
save(configPath, config);
|
|
15
|
+
vapidPublicKey = keys.publicKey;
|
|
16
|
+
webpush.setVapidDetails('mailto:noreply@claude-remote-cli.local', keys.publicKey, keys.privateKey);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// VAPID key generation failed — push will be unavailable
|
|
20
|
+
vapidPublicKey = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function getVapidPublicKey() {
|
|
24
|
+
return vapidPublicKey;
|
|
25
|
+
}
|
|
26
|
+
export function subscribe(subscription, sessionIds) {
|
|
27
|
+
// Replace the full session list for this endpoint — the client sends
|
|
28
|
+
// the complete set of sessions it wants notifications for.
|
|
29
|
+
subscriptions.set(subscription.endpoint, {
|
|
30
|
+
subscription,
|
|
31
|
+
sessionIds: new Set(sessionIds),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function unsubscribe(endpoint) {
|
|
35
|
+
subscriptions.delete(endpoint);
|
|
36
|
+
}
|
|
37
|
+
export function removeSession(sessionId) {
|
|
38
|
+
for (const entry of subscriptions.values()) {
|
|
39
|
+
entry.sessionIds.delete(sessionId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function notifySessionIdle(sessionId, session) {
|
|
43
|
+
if (!vapidPublicKey)
|
|
44
|
+
return;
|
|
45
|
+
const payload = JSON.stringify({
|
|
46
|
+
type: 'session-attention',
|
|
47
|
+
sessionId,
|
|
48
|
+
displayName: session.displayName,
|
|
49
|
+
sessionType: session.type,
|
|
50
|
+
});
|
|
51
|
+
for (const [endpoint, entry] of subscriptions) {
|
|
52
|
+
if (!entry.sessionIds.has(sessionId))
|
|
53
|
+
continue;
|
|
54
|
+
webpush.sendNotification(entry.subscription, payload).catch((err) => {
|
|
55
|
+
if (err.statusCode === 410 || err.statusCode === 404) {
|
|
56
|
+
subscriptions.delete(endpoint);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/server/sessions.js
CHANGED
|
@@ -38,9 +38,9 @@ function resolveTmuxSpawn(command, args, tmuxSessionName) {
|
|
|
38
38
|
const sessions = new Map();
|
|
39
39
|
const IDLE_TIMEOUT_MS = 5000;
|
|
40
40
|
let terminalCounter = 0;
|
|
41
|
-
|
|
41
|
+
const idleChangeCallbacks = [];
|
|
42
42
|
function onIdleChange(cb) {
|
|
43
|
-
|
|
43
|
+
idleChangeCallbacks.push(cb);
|
|
44
44
|
}
|
|
45
45
|
function create({ type, agent = 'claude', repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath, useTmux: paramUseTmux }) {
|
|
46
46
|
const id = crypto.randomBytes(8).toString('hex');
|
|
@@ -102,16 +102,16 @@ function create({ type, agent = 'claude', repoName, repoPath, cwd, root, worktre
|
|
|
102
102
|
function resetIdleTimer() {
|
|
103
103
|
if (session.idle) {
|
|
104
104
|
session.idle = false;
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
for (const cb of idleChangeCallbacks)
|
|
106
|
+
cb(session.id, false);
|
|
107
107
|
}
|
|
108
108
|
if (idleTimer)
|
|
109
109
|
clearTimeout(idleTimer);
|
|
110
110
|
idleTimer = setTimeout(() => {
|
|
111
111
|
if (!session.idle) {
|
|
112
112
|
session.idle = true;
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
for (const cb of idleChangeCallbacks)
|
|
114
|
+
cb(session.id, true);
|
|
115
115
|
}
|
|
116
116
|
}, IDLE_TIMEOUT_MS);
|
|
117
117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-remote-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "Remote web interface for Claude Code CLI sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server/index.js",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"express": "^4.21.0",
|
|
51
51
|
"node-pty": "^1.0.0",
|
|
52
52
|
"svelte": "^5.53.3",
|
|
53
|
+
"web-push": "^3.6.7",
|
|
53
54
|
"ws": "^8.18.0"
|
|
54
55
|
},
|
|
55
56
|
"devDependencies": {
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"@types/cookie-parser": "^1.4.7",
|
|
60
61
|
"@types/express": "^4.17.21",
|
|
61
62
|
"@types/node": "^22.0.0",
|
|
63
|
+
"@types/web-push": "^3.6.4",
|
|
62
64
|
"@types/ws": "^8.5.13",
|
|
63
65
|
"playwright": "^1.58.2",
|
|
64
66
|
"svelte-check": "^4.4.3",
|