claude-remote-cli 0.3.0 → 1.1.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.
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import * as service from '../server/service.js';
6
+ import { DEFAULTS } from '../server/config.js';
7
+ const require = createRequire(import.meta.url);
8
+ // Parse CLI flags
9
+ const args = process.argv.slice(2);
10
+ if (args.includes('--help') || args.includes('-h')) {
11
+ console.log(`Usage: claude-remote-cli [options]
12
+ claude-remote-cli <command>
13
+
14
+ Commands:
15
+ install Install as a background service (survives reboot)
16
+ uninstall Stop and remove the background service
17
+ status Show whether the service is running
18
+
19
+ Options:
20
+ --bg Shortcut: install and start as background service
21
+ --port <port> Override server port (default: 3456)
22
+ --host <host> Override bind address (default: 0.0.0.0)
23
+ --config <path> Path to config.json (default: ~/.config/claude-remote-cli/config.json)
24
+ --version, -v Show version
25
+ --help, -h Show this help`);
26
+ process.exit(0);
27
+ }
28
+ if (args.includes('--version') || args.includes('-v')) {
29
+ const pkg = require('../package.json');
30
+ console.log(pkg.version);
31
+ process.exit(0);
32
+ }
33
+ function getArg(flag) {
34
+ const idx = args.indexOf(flag);
35
+ if (idx === -1 || idx + 1 >= args.length)
36
+ return undefined;
37
+ return args[idx + 1];
38
+ }
39
+ function resolveConfigPath() {
40
+ const explicit = getArg('--config');
41
+ if (explicit)
42
+ return explicit;
43
+ return path.join(service.CONFIG_DIR, 'config.json');
44
+ }
45
+ function runServiceCommand(fn) {
46
+ try {
47
+ fn();
48
+ }
49
+ catch (e) {
50
+ console.error(e.message);
51
+ process.exit(1);
52
+ }
53
+ process.exit(0);
54
+ }
55
+ const command = args[0];
56
+ if (command === 'install' || command === 'uninstall' || command === 'status' || args.includes('--bg')) {
57
+ if (command === 'uninstall') {
58
+ runServiceCommand(() => { service.uninstall(); });
59
+ }
60
+ else if (command === 'status') {
61
+ runServiceCommand(() => {
62
+ const st = service.status();
63
+ if (!st.installed) {
64
+ console.log('Service is not installed.');
65
+ }
66
+ else if (st.running) {
67
+ console.log('Service is installed and running.');
68
+ }
69
+ else {
70
+ console.log('Service is installed but not running.');
71
+ }
72
+ });
73
+ }
74
+ else {
75
+ runServiceCommand(() => {
76
+ service.install({
77
+ configPath: resolveConfigPath(),
78
+ port: getArg('--port') ?? String(DEFAULTS.port),
79
+ host: getArg('--host') ?? DEFAULTS.host,
80
+ });
81
+ });
82
+ }
83
+ }
84
+ const configPath = resolveConfigPath();
85
+ const configDir = path.dirname(configPath);
86
+ // Ensure config directory exists
87
+ if (!fs.existsSync(configDir)) {
88
+ fs.mkdirSync(configDir, { recursive: true });
89
+ }
90
+ // Pass config path and CLI overrides to the server
91
+ process.env['CLAUDE_REMOTE_CONFIG'] = configPath;
92
+ const portArg = getArg('--port');
93
+ if (portArg !== undefined)
94
+ process.env['CLAUDE_REMOTE_PORT'] = portArg;
95
+ const hostArg = getArg('--host');
96
+ if (hostArg !== undefined)
97
+ process.env['CLAUDE_REMOTE_HOST'] = hostArg;
98
+ await import('../server/index.js');
@@ -0,0 +1,41 @@
1
+ import bcrypt from 'bcrypt';
2
+ import crypto from 'node:crypto';
3
+ const SALT_ROUNDS = 10;
4
+ const MAX_ATTEMPTS = 5;
5
+ const LOCKOUT_DURATION_MS = 15 * 60 * 1000; // 15 minutes
6
+ const attemptMap = new Map();
7
+ export async function hashPin(pin) {
8
+ return bcrypt.hash(pin, SALT_ROUNDS);
9
+ }
10
+ export async function verifyPin(pin, hash) {
11
+ return bcrypt.compare(pin, hash);
12
+ }
13
+ export function isRateLimited(ip) {
14
+ const entry = attemptMap.get(ip);
15
+ if (!entry)
16
+ return false;
17
+ if (entry.lockedUntil) {
18
+ if (Date.now() < entry.lockedUntil) {
19
+ return true;
20
+ }
21
+ attemptMap.delete(ip);
22
+ }
23
+ return false;
24
+ }
25
+ export function recordFailedAttempt(ip) {
26
+ const entry = attemptMap.get(ip) ?? { count: 0, lockedUntil: null };
27
+ entry.count += 1;
28
+ if (entry.count >= MAX_ATTEMPTS) {
29
+ entry.lockedUntil = Date.now() + LOCKOUT_DURATION_MS;
30
+ }
31
+ attemptMap.set(ip, entry);
32
+ }
33
+ export function clearRateLimit(ip) {
34
+ attemptMap.delete(ip);
35
+ }
36
+ export function generateCookieToken() {
37
+ return crypto.randomBytes(32).toString('hex');
38
+ }
39
+ export function _resetForTesting() {
40
+ attemptMap.clear();
41
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs';
2
+ export const DEFAULTS = {
3
+ host: '0.0.0.0',
4
+ port: 3456,
5
+ cookieTTL: '24h',
6
+ repos: [],
7
+ claudeCommand: 'claude',
8
+ claudeArgs: [],
9
+ };
10
+ export function loadConfig(configPath) {
11
+ if (!fs.existsSync(configPath)) {
12
+ throw new Error(`Config file not found: ${configPath}`);
13
+ }
14
+ const raw = fs.readFileSync(configPath, 'utf8');
15
+ const parsed = JSON.parse(raw);
16
+ return { ...DEFAULTS, ...parsed };
17
+ }
18
+ export function saveConfig(configPath, config) {
19
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
20
+ }
@@ -0,0 +1,296 @@
1
+ import fs from 'node:fs';
2
+ import http from 'node:http';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import readline from 'node:readline';
6
+ import express from 'express';
7
+ import cookieParser from 'cookie-parser';
8
+ import { loadConfig, saveConfig, DEFAULTS } from './config.js';
9
+ import * as auth from './auth.js';
10
+ import * as sessions from './sessions.js';
11
+ import { setupWebSocket } from './ws.js';
12
+ import { WorktreeWatcher } from './watcher.js';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ // When run via CLI bin, config lives in ~/.config/claude-remote-cli/
16
+ // When run directly (development), fall back to local config.json
17
+ const CONFIG_PATH = process.env.CLAUDE_REMOTE_CONFIG || path.join(__dirname, '..', 'config.json');
18
+ function parseTTL(ttl) {
19
+ if (typeof ttl !== 'string')
20
+ return 24 * 60 * 60 * 1000;
21
+ const match = ttl.match(/^(\d+)([smhd])$/);
22
+ if (!match)
23
+ return 24 * 60 * 60 * 1000;
24
+ const value = parseInt(match[1], 10);
25
+ switch (match[2]) {
26
+ case 's': return value * 1000;
27
+ case 'm': return value * 60 * 1000;
28
+ case 'h': return value * 60 * 60 * 1000;
29
+ case 'd': return value * 24 * 60 * 60 * 1000;
30
+ default: return 24 * 60 * 60 * 1000;
31
+ }
32
+ }
33
+ function promptPin(question) {
34
+ return new Promise((resolve) => {
35
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
36
+ rl.question(question, (answer) => {
37
+ rl.close();
38
+ resolve(answer.trim());
39
+ });
40
+ });
41
+ }
42
+ function scanReposInRoot(rootDir) {
43
+ const repos = [];
44
+ let entries;
45
+ try {
46
+ entries = fs.readdirSync(rootDir, { withFileTypes: true });
47
+ }
48
+ catch (_) {
49
+ return repos;
50
+ }
51
+ for (const entry of entries) {
52
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
53
+ continue;
54
+ const fullPath = path.join(rootDir, entry.name);
55
+ if (fs.existsSync(path.join(fullPath, '.git'))) {
56
+ repos.push({ name: entry.name, path: fullPath, root: rootDir });
57
+ }
58
+ }
59
+ return repos;
60
+ }
61
+ function scanAllRepos(rootDirs) {
62
+ const repos = [];
63
+ for (const rootDir of rootDirs) {
64
+ repos.push(...scanReposInRoot(rootDir));
65
+ }
66
+ return repos;
67
+ }
68
+ async function main() {
69
+ let config;
70
+ try {
71
+ config = loadConfig(CONFIG_PATH);
72
+ }
73
+ catch (_) {
74
+ config = { ...DEFAULTS };
75
+ saveConfig(CONFIG_PATH, config);
76
+ }
77
+ // CLI flag overrides
78
+ if (process.env.CLAUDE_REMOTE_PORT)
79
+ config.port = parseInt(process.env.CLAUDE_REMOTE_PORT, 10);
80
+ if (process.env.CLAUDE_REMOTE_HOST)
81
+ config.host = process.env.CLAUDE_REMOTE_HOST;
82
+ if (!config.pinHash) {
83
+ const pin = await promptPin('Set up a PIN for claude-remote-cli:');
84
+ config.pinHash = await auth.hashPin(pin);
85
+ saveConfig(CONFIG_PATH, config);
86
+ console.log('PIN set successfully.');
87
+ }
88
+ const authenticatedTokens = new Set();
89
+ const app = express();
90
+ app.use(express.json());
91
+ app.use(cookieParser());
92
+ app.use(express.static(path.join(__dirname, '..', 'public')));
93
+ const requireAuth = (req, res, next) => {
94
+ const token = req.cookies && req.cookies.token;
95
+ if (!token || !authenticatedTokens.has(token)) {
96
+ res.status(401).json({ error: 'Unauthorized' });
97
+ return;
98
+ }
99
+ next();
100
+ };
101
+ const watcher = new WorktreeWatcher();
102
+ watcher.rebuild(config.rootDirs || []);
103
+ const server = http.createServer(app);
104
+ const { broadcastEvent } = setupWebSocket(server, authenticatedTokens, watcher);
105
+ // POST /auth
106
+ app.post('/auth', async (req, res) => {
107
+ const ip = (req.ip || req.connection.remoteAddress);
108
+ if (auth.isRateLimited(ip)) {
109
+ res.status(429).json({ error: 'Too many attempts. Try again later.' });
110
+ return;
111
+ }
112
+ const { pin } = req.body;
113
+ if (!pin) {
114
+ res.status(400).json({ error: 'PIN required' });
115
+ return;
116
+ }
117
+ const valid = await auth.verifyPin(pin, config.pinHash);
118
+ if (!valid) {
119
+ auth.recordFailedAttempt(ip);
120
+ res.status(401).json({ error: 'Invalid PIN' });
121
+ return;
122
+ }
123
+ auth.clearRateLimit(ip);
124
+ const token = auth.generateCookieToken();
125
+ authenticatedTokens.add(token);
126
+ const ttlMs = parseTTL(config.cookieTTL);
127
+ setTimeout(() => authenticatedTokens.delete(token), ttlMs);
128
+ res.cookie('token', token, {
129
+ httpOnly: true,
130
+ sameSite: 'strict',
131
+ maxAge: ttlMs,
132
+ });
133
+ res.json({ ok: true });
134
+ });
135
+ // GET /sessions
136
+ app.get('/sessions', requireAuth, (_req, res) => {
137
+ res.json(sessions.list());
138
+ });
139
+ // GET /repos — scan root dirs for repos
140
+ app.get('/repos', requireAuth, (_req, res) => {
141
+ const repos = scanAllRepos(config.rootDirs || []);
142
+ // Also include legacy manually-added repos
143
+ if (config.repos) {
144
+ for (const repo of config.repos) {
145
+ if (!repos.some((r) => r.path === repo.path)) {
146
+ repos.push(repo);
147
+ }
148
+ }
149
+ }
150
+ res.json(repos);
151
+ });
152
+ // GET /worktrees?repo=<path> — list worktrees; omit repo to scan all repos in all rootDirs
153
+ app.get('/worktrees', requireAuth, (req, res) => {
154
+ const repoParam = typeof req.query.repo === 'string' ? req.query.repo : undefined;
155
+ const roots = config.rootDirs || [];
156
+ const worktrees = [];
157
+ let reposToScan;
158
+ if (repoParam) {
159
+ const root = roots.find(function (r) { return repoParam.startsWith(r); }) || '';
160
+ reposToScan = [{ path: repoParam, name: repoParam.split('/').filter(Boolean).pop() || '', root }];
161
+ }
162
+ else {
163
+ reposToScan = scanAllRepos(roots);
164
+ }
165
+ for (const repo of reposToScan) {
166
+ const worktreeDir = path.join(repo.path, '.claude', 'worktrees');
167
+ let entries;
168
+ try {
169
+ entries = fs.readdirSync(worktreeDir, { withFileTypes: true });
170
+ }
171
+ catch (_) {
172
+ continue;
173
+ }
174
+ for (const entry of entries) {
175
+ if (!entry.isDirectory())
176
+ continue;
177
+ worktrees.push({
178
+ name: entry.name,
179
+ path: path.join(worktreeDir, entry.name),
180
+ repoName: repo.name,
181
+ repoPath: repo.path,
182
+ root: repo.root,
183
+ });
184
+ }
185
+ }
186
+ res.json(worktrees);
187
+ });
188
+ // GET /roots — list root directories
189
+ app.get('/roots', requireAuth, (_req, res) => {
190
+ res.json(config.rootDirs || []);
191
+ });
192
+ // POST /roots — add a root directory
193
+ app.post('/roots', requireAuth, (req, res) => {
194
+ const { path: rootPath } = req.body;
195
+ if (!rootPath) {
196
+ res.status(400).json({ error: 'path is required' });
197
+ return;
198
+ }
199
+ if (!config.rootDirs)
200
+ config.rootDirs = [];
201
+ if (config.rootDirs.includes(rootPath)) {
202
+ res.status(409).json({ error: 'Root already exists' });
203
+ return;
204
+ }
205
+ config.rootDirs.push(rootPath);
206
+ saveConfig(CONFIG_PATH, config);
207
+ watcher.rebuild(config.rootDirs);
208
+ broadcastEvent('worktrees-changed');
209
+ res.status(201).json(config.rootDirs);
210
+ });
211
+ // DELETE /roots — remove a root directory
212
+ app.delete('/roots', requireAuth, (req, res) => {
213
+ const { path: rootPath } = req.body;
214
+ if (!rootPath || !config.rootDirs) {
215
+ res.status(400).json({ error: 'path is required' });
216
+ return;
217
+ }
218
+ config.rootDirs = config.rootDirs.filter((r) => r !== rootPath);
219
+ saveConfig(CONFIG_PATH, config);
220
+ watcher.rebuild(config.rootDirs);
221
+ broadcastEvent('worktrees-changed');
222
+ res.json(config.rootDirs);
223
+ });
224
+ // POST /sessions
225
+ app.post('/sessions', requireAuth, (req, res) => {
226
+ const { repoPath, repoName, worktreePath, claudeArgs } = req.body;
227
+ if (!repoPath) {
228
+ res.status(400).json({ error: 'repoPath is required' });
229
+ return;
230
+ }
231
+ const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
232
+ const baseArgs = claudeArgs || config.claudeArgs || [];
233
+ // Compute root by matching repoPath against configured rootDirs
234
+ const roots = config.rootDirs || [];
235
+ const root = roots.find(function (r) { return repoPath.startsWith(r); }) || '';
236
+ let args;
237
+ let cwd;
238
+ let worktreeName;
239
+ if (worktreePath) {
240
+ // Resume existing worktree — run claude inside the worktree directory
241
+ args = [...baseArgs];
242
+ cwd = worktreePath;
243
+ worktreeName = worktreePath.split('/').pop() || '';
244
+ }
245
+ else {
246
+ // New worktree
247
+ worktreeName = 'mobile-' + name + '-' + Date.now().toString(36);
248
+ args = ['--worktree', worktreeName, ...baseArgs];
249
+ cwd = repoPath;
250
+ }
251
+ const session = sessions.create({
252
+ repoName: name,
253
+ repoPath: cwd,
254
+ root,
255
+ worktreeName,
256
+ displayName: worktreeName,
257
+ command: config.claudeCommand,
258
+ args,
259
+ });
260
+ res.status(201).json(session);
261
+ });
262
+ // DELETE /sessions/:id
263
+ app.delete('/sessions/:id', requireAuth, (req, res) => {
264
+ try {
265
+ sessions.kill(req.params['id']);
266
+ res.json({ ok: true });
267
+ }
268
+ catch (_) {
269
+ res.status(404).json({ error: 'Session not found' });
270
+ }
271
+ });
272
+ // PATCH /sessions/:id — update displayName and send /rename through PTY
273
+ app.patch('/sessions/:id', requireAuth, (req, res) => {
274
+ const { displayName } = req.body;
275
+ if (!displayName) {
276
+ res.status(400).json({ error: 'displayName is required' });
277
+ return;
278
+ }
279
+ try {
280
+ const id = req.params['id'];
281
+ const updated = sessions.updateDisplayName(id, displayName);
282
+ const session = sessions.get(id);
283
+ if (session && session.pty) {
284
+ session.pty.write('/rename "' + displayName.replace(/"/g, '\\"') + '"\r');
285
+ }
286
+ res.json(updated);
287
+ }
288
+ catch (_) {
289
+ res.status(404).json({ error: 'Session not found' });
290
+ }
291
+ });
292
+ server.listen(config.port, config.host, () => {
293
+ console.log(`claude-remote-cli listening on ${config.host}:${config.port}`);
294
+ });
295
+ }
296
+ main().catch(console.error);
@@ -0,0 +1,169 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { execSync } from 'node:child_process';
5
+ import { DEFAULTS } from './config.js';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const SERVICE_LABEL = 'com.claude-remote-cli';
9
+ const HOME = process.env.HOME || process.env.USERPROFILE || '~';
10
+ const CONFIG_DIR = path.join(HOME, '.config', 'claude-remote-cli');
11
+ function getPlatform() {
12
+ if (process.platform === 'darwin')
13
+ return 'macos';
14
+ if (process.platform === 'linux')
15
+ return 'linux';
16
+ throw new Error('Unsupported platform: ' + process.platform + '. Only macOS and Linux are supported.');
17
+ }
18
+ function getServicePaths() {
19
+ const platform = getPlatform();
20
+ if (platform === 'macos') {
21
+ return {
22
+ servicePath: path.join(HOME, 'Library', 'LaunchAgents', SERVICE_LABEL + '.plist'),
23
+ logDir: path.join(CONFIG_DIR, 'logs'),
24
+ label: SERVICE_LABEL,
25
+ };
26
+ }
27
+ return {
28
+ servicePath: path.join(HOME, '.config', 'systemd', 'user', 'claude-remote-cli.service'),
29
+ logDir: null,
30
+ label: 'claude-remote-cli',
31
+ };
32
+ }
33
+ function generateServiceFile(platform, opts) {
34
+ const { nodePath, scriptPath, configPath, port, host, logDir } = opts;
35
+ if (platform === 'macos') {
36
+ return `<?xml version="1.0" encoding="UTF-8"?>
37
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
38
+ <plist version="1.0">
39
+ <dict>
40
+ <key>Label</key>
41
+ <string>${SERVICE_LABEL}</string>
42
+ <key>ProgramArguments</key>
43
+ <array>
44
+ <string>${nodePath}</string>
45
+ <string>${scriptPath}</string>
46
+ <string>--config</string>
47
+ <string>${configPath}</string>
48
+ <string>--port</string>
49
+ <string>${port}</string>
50
+ <string>--host</string>
51
+ <string>${host}</string>
52
+ </array>
53
+ <key>RunAtLoad</key>
54
+ <true/>
55
+ <key>KeepAlive</key>
56
+ <true/>
57
+ <key>StandardOutPath</key>
58
+ <string>${path.join(logDir, 'stdout.log')}</string>
59
+ <key>StandardErrorPath</key>
60
+ <string>${path.join(logDir, 'stderr.log')}</string>
61
+ <key>EnvironmentVariables</key>
62
+ <dict>
63
+ <key>PATH</key>
64
+ <string>${process.env.PATH}</string>
65
+ </dict>
66
+ </dict>
67
+ </plist>`;
68
+ }
69
+ return `[Unit]
70
+ Description=Claude Remote CLI
71
+ After=network.target
72
+
73
+ [Service]
74
+ Type=simple
75
+ ExecStart=${nodePath} ${scriptPath} --config ${configPath} --port ${port} --host ${host}
76
+ Restart=on-failure
77
+ RestartSec=5
78
+ Environment=PATH=${process.env.PATH}
79
+
80
+ [Install]
81
+ WantedBy=default.target`;
82
+ }
83
+ function isInstalled() {
84
+ const { servicePath } = getServicePaths();
85
+ return fs.existsSync(servicePath);
86
+ }
87
+ function install(opts) {
88
+ const platform = getPlatform();
89
+ const { servicePath, logDir } = getServicePaths();
90
+ if (isInstalled()) {
91
+ throw new Error('Service is already installed. Run `claude-remote-cli uninstall` first.');
92
+ }
93
+ const nodePath = process.execPath;
94
+ const scriptPath = path.resolve(__dirname, '..', 'bin', 'claude-remote-cli.js');
95
+ const configPath = opts.configPath || path.join(CONFIG_DIR, 'config.json');
96
+ const port = opts.port || String(DEFAULTS.port);
97
+ const host = opts.host || DEFAULTS.host;
98
+ const content = generateServiceFile(platform, { nodePath, scriptPath, configPath, port, host, logDir });
99
+ fs.mkdirSync(path.dirname(servicePath), { recursive: true });
100
+ if (logDir)
101
+ fs.mkdirSync(logDir, { recursive: true });
102
+ fs.writeFileSync(servicePath, content, 'utf8');
103
+ if (platform === 'macos') {
104
+ execSync('launchctl load -w ' + servicePath, { stdio: 'inherit' });
105
+ }
106
+ else {
107
+ execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
108
+ execSync('systemctl --user enable --now claude-remote-cli', { stdio: 'inherit' });
109
+ }
110
+ console.log('Service installed and started.');
111
+ if (logDir) {
112
+ console.log('Logs: ' + logDir);
113
+ }
114
+ else {
115
+ console.log('Logs: journalctl --user -u claude-remote-cli -f');
116
+ }
117
+ }
118
+ function uninstall() {
119
+ const platform = getPlatform();
120
+ const { servicePath } = getServicePaths();
121
+ if (!isInstalled()) {
122
+ throw new Error('Service is not installed.');
123
+ }
124
+ if (platform === 'macos') {
125
+ try {
126
+ execSync('launchctl unload ' + servicePath, { stdio: 'inherit' });
127
+ }
128
+ catch (_) {
129
+ // Ignore errors from already-unloaded services
130
+ }
131
+ }
132
+ else {
133
+ try {
134
+ execSync('systemctl --user disable --now claude-remote-cli', { stdio: 'inherit' });
135
+ }
136
+ catch (_) {
137
+ // Ignore errors from already-disabled services
138
+ }
139
+ }
140
+ fs.unlinkSync(servicePath);
141
+ console.log('Service uninstalled.');
142
+ }
143
+ function status() {
144
+ const platform = getPlatform();
145
+ if (!isInstalled()) {
146
+ return { installed: false, running: false };
147
+ }
148
+ const running = checkRunning(platform);
149
+ return { installed: true, running };
150
+ }
151
+ function checkRunning(platform) {
152
+ if (platform === 'macos') {
153
+ try {
154
+ const out = execSync('launchctl list ' + SERVICE_LABEL, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
155
+ return !out.includes('"LastExitStatus" = -1');
156
+ }
157
+ catch (_) {
158
+ return false;
159
+ }
160
+ }
161
+ try {
162
+ execSync('systemctl --user is-active claude-remote-cli', { stdio: ['pipe', 'pipe', 'pipe'] });
163
+ return true;
164
+ }
165
+ catch (_) {
166
+ return false;
167
+ }
168
+ }
169
+ export { getPlatform, getServicePaths, generateServiceFile, isInstalled, install, uninstall, status, SERVICE_LABEL, CONFIG_DIR };