claude-remote-cli 2.7.0 → 2.9.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.
@@ -11,8 +11,8 @@
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-C2nVSRxb.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-Not5cXLa.css">
14
+ <script type="module" crossorigin src="/assets/index-Dkj00kFR.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-B3mDW63X.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
@@ -8,6 +8,7 @@ export const DEFAULTS = {
8
8
  repos: [],
9
9
  claudeCommand: 'claude',
10
10
  claudeArgs: [],
11
+ defaultAgent: 'claude',
11
12
  };
12
13
  export function loadConfig(configPath) {
13
14
  if (!fs.existsSync(configPath)) {
@@ -11,8 +11,9 @@ import cookieParser from 'cookie-parser';
11
11
  import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, deleteMeta, ensureMetaDir } from './config.js';
12
12
  import * as auth from './auth.js';
13
13
  import * as sessions from './sessions.js';
14
+ import { AGENT_CONTINUE_ARGS } from './sessions.js';
14
15
  import { setupWebSocket } from './ws.js';
15
- import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain } from './watcher.js';
16
+ import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain, parseAllWorktrees } from './watcher.js';
16
17
  import { isInstalled as serviceIsInstalled } from './service.js';
17
18
  import { extensionForMime, setClipboardImage } from './clipboard.js';
18
19
  const __filename = fileURLToPath(import.meta.url);
@@ -500,6 +501,21 @@ async function main() {
500
501
  broadcastEvent('worktrees-changed');
501
502
  res.json(config.rootDirs);
502
503
  });
504
+ // GET /config/defaultAgent — get default coding agent
505
+ app.get('/config/defaultAgent', requireAuth, (_req, res) => {
506
+ res.json({ defaultAgent: config.defaultAgent || 'claude' });
507
+ });
508
+ // PATCH /config/defaultAgent — set default coding agent
509
+ app.patch('/config/defaultAgent', requireAuth, (req, res) => {
510
+ const { defaultAgent } = req.body;
511
+ if (!defaultAgent || (defaultAgent !== 'claude' && defaultAgent !== 'codex')) {
512
+ res.status(400).json({ error: 'defaultAgent must be "claude" or "codex"' });
513
+ return;
514
+ }
515
+ config.defaultAgent = defaultAgent;
516
+ saveConfig(CONFIG_PATH, config);
517
+ res.json({ defaultAgent: config.defaultAgent });
518
+ });
503
519
  // DELETE /worktrees — remove a worktree, prune, and delete its branch
504
520
  app.delete('/worktrees', requireAuth, async (req, res) => {
505
521
  const { worktreePath, repoPath } = req.body;
@@ -507,9 +523,22 @@ async function main() {
507
523
  res.status(400).json({ error: 'worktreePath and repoPath are required' });
508
524
  return;
509
525
  }
510
- if (!isValidWorktreePath(worktreePath)) {
511
- res.status(400).json({ error: 'Path is not inside a worktree directory' });
512
- return;
526
+ // Validate the path is a real git worktree (not the main worktree)
527
+ try {
528
+ const { stdout: wtListOut } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repoPath });
529
+ const allWorktrees = parseAllWorktrees(wtListOut, repoPath);
530
+ const isKnownWorktree = allWorktrees.some(wt => wt.path === path.resolve(worktreePath) && !wt.isMain);
531
+ if (!isKnownWorktree) {
532
+ res.status(400).json({ error: 'Path is not a recognized git worktree' });
533
+ return;
534
+ }
535
+ }
536
+ catch {
537
+ // If git worktree list fails, fall back to the directory-name check
538
+ if (!isValidWorktreePath(worktreePath)) {
539
+ res.status(400).json({ error: 'Path is not inside a worktree directory' });
540
+ return;
541
+ }
513
542
  }
514
543
  // Check no active session is using this worktree
515
544
  const activeSessions = sessions.list();
@@ -561,11 +590,12 @@ async function main() {
561
590
  });
562
591
  // POST /sessions
563
592
  app.post('/sessions', requireAuth, async (req, res) => {
564
- const { repoPath, repoName, worktreePath, branchName, claudeArgs } = req.body;
593
+ const { repoPath, repoName, worktreePath, branchName, claudeArgs, agent } = req.body;
565
594
  if (!repoPath) {
566
595
  res.status(400).json({ error: 'repoPath is required' });
567
596
  return;
568
597
  }
598
+ const resolvedAgent = agent || config.defaultAgent || 'claude';
569
599
  const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
570
600
  const baseArgs = [...(config.claudeArgs || []), ...(claudeArgs || [])];
571
601
  // Compute root by matching repoPath against configured rootDirs
@@ -578,7 +608,7 @@ async function main() {
578
608
  let resolvedBranch = '';
579
609
  if (worktreePath) {
580
610
  // Resume existing worktree
581
- args = ['--continue', ...baseArgs];
611
+ args = [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs];
582
612
  cwd = worktreePath;
583
613
  sessionRepoPath = worktreePath;
584
614
  worktreeName = worktreePath.split('/').pop() || '';
@@ -620,6 +650,62 @@ async function main() {
620
650
  }
621
651
  }
622
652
  if (branchName && branchExists) {
653
+ // Check if branch is already checked out in an existing worktree
654
+ const { stdout: wtListOut } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repoPath });
655
+ const allWorktrees = parseAllWorktrees(wtListOut, repoPath);
656
+ const existingWt = allWorktrees.find(wt => wt.branch === branchName);
657
+ if (existingWt) {
658
+ // Branch already checked out — redirect to the existing worktree
659
+ if (existingWt.isMain) {
660
+ // Main worktree → create a repo session
661
+ const existingRepoSession = sessions.findRepoSession(repoPath);
662
+ if (existingRepoSession) {
663
+ res.status(409).json({ error: 'A session already exists for this repo', sessionId: existingRepoSession.id });
664
+ return;
665
+ }
666
+ const repoSession = sessions.create({
667
+ type: 'repo',
668
+ agent: resolvedAgent,
669
+ repoName: name,
670
+ repoPath,
671
+ cwd: repoPath,
672
+ root,
673
+ displayName: name,
674
+ args: baseArgs,
675
+ });
676
+ res.status(201).json(repoSession);
677
+ return;
678
+ }
679
+ else {
680
+ // Another worktree → create a worktree session with --continue
681
+ cwd = existingWt.path;
682
+ sessionRepoPath = existingWt.path;
683
+ worktreeName = existingWt.path.split('/').pop() || '';
684
+ args = [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs];
685
+ const displayNameVal = branchName || worktreeName;
686
+ const session = sessions.create({
687
+ type: 'worktree',
688
+ agent: resolvedAgent,
689
+ repoName: name,
690
+ repoPath: sessionRepoPath,
691
+ cwd,
692
+ root,
693
+ worktreeName,
694
+ branchName: branchName || worktreeName,
695
+ displayName: displayNameVal,
696
+ args,
697
+ configPath: CONFIG_PATH,
698
+ });
699
+ writeMeta(CONFIG_PATH, {
700
+ worktreePath: sessionRepoPath,
701
+ displayName: displayNameVal,
702
+ lastActivity: new Date().toISOString(),
703
+ branchName: branchName || worktreeName,
704
+ });
705
+ res.status(201).json(session);
706
+ return;
707
+ }
708
+ }
623
709
  await execFileAsync('git', ['worktree', 'add', targetDir, resolvedBranch], { cwd: repoPath });
624
710
  }
625
711
  else if (branchName) {
@@ -641,6 +727,7 @@ async function main() {
641
727
  const displayName = branchName || worktreeName;
642
728
  const session = sessions.create({
643
729
  type: 'worktree',
730
+ agent: resolvedAgent,
644
731
  repoName: name,
645
732
  repoPath: sessionRepoPath,
646
733
  cwd,
@@ -648,7 +735,6 @@ async function main() {
648
735
  worktreeName,
649
736
  branchName: branchName || worktreeName,
650
737
  displayName,
651
- command: config.claudeCommand,
652
738
  args,
653
739
  configPath: CONFIG_PATH,
654
740
  });
@@ -664,11 +750,12 @@ async function main() {
664
750
  });
665
751
  // POST /sessions/repo — start a session in the repo root (no worktree)
666
752
  app.post('/sessions/repo', requireAuth, (req, res) => {
667
- const { repoPath, repoName, continue: continueSession, claudeArgs } = req.body;
753
+ const { repoPath, repoName, continue: continueSession, claudeArgs, agent } = req.body;
668
754
  if (!repoPath) {
669
755
  res.status(400).json({ error: 'repoPath is required' });
670
756
  return;
671
757
  }
758
+ const resolvedAgent = agent || config.defaultAgent || 'claude';
672
759
  // One repo session at a time
673
760
  const existing = sessions.findRepoSession(repoPath);
674
761
  if (existing) {
@@ -677,17 +764,17 @@ async function main() {
677
764
  }
678
765
  const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
679
766
  const baseArgs = [...(config.claudeArgs || []), ...(claudeArgs || [])];
680
- const args = continueSession ? ['--continue', ...baseArgs] : [...baseArgs];
767
+ const args = continueSession ? [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs] : [...baseArgs];
681
768
  const roots = config.rootDirs || [];
682
769
  const root = roots.find(function (r) { return repoPath.startsWith(r); }) || '';
683
770
  const session = sessions.create({
684
771
  type: 'repo',
772
+ agent: resolvedAgent,
685
773
  repoName: name,
686
774
  repoPath,
687
775
  cwd: repoPath,
688
776
  root,
689
777
  displayName: name,
690
- command: config.claudeCommand,
691
778
  args,
692
779
  });
693
780
  res.status(201).json(session);
@@ -4,6 +4,18 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { readMeta, writeMeta } from './config.js';
7
+ const AGENT_COMMANDS = {
8
+ claude: 'claude',
9
+ codex: 'codex',
10
+ };
11
+ const AGENT_YOLO_ARGS = {
12
+ claude: ['--dangerously-skip-permissions'],
13
+ codex: ['--full-auto'],
14
+ };
15
+ const AGENT_CONTINUE_ARGS = {
16
+ claude: ['--continue'],
17
+ codex: ['resume', '--last'],
18
+ };
7
19
  // In-memory registry: id -> Session
8
20
  const sessions = new Map();
9
21
  const IDLE_TIMEOUT_MS = 5000;
@@ -11,13 +23,14 @@ let idleChangeCallback = null;
11
23
  function onIdleChange(cb) {
12
24
  idleChangeCallback = cb;
13
25
  }
14
- function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
26
+ function create({ type, agent = 'claude', repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
15
27
  const id = crypto.randomBytes(8).toString('hex');
16
28
  const createdAt = new Date().toISOString();
29
+ const resolvedCommand = command || AGENT_COMMANDS[agent];
17
30
  // Strip CLAUDECODE env var to allow spawning claude inside a claude-managed server
18
31
  const env = Object.assign({}, process.env);
19
32
  delete env.CLAUDECODE;
20
- const ptyProcess = pty.spawn(command, args, {
33
+ const ptyProcess = pty.spawn(resolvedCommand, args, {
21
34
  name: 'xterm-256color',
22
35
  cols,
23
36
  rows,
@@ -31,6 +44,7 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName,
31
44
  const session = {
32
45
  id,
33
46
  type: type || 'worktree',
47
+ agent,
34
48
  root: root || '',
35
49
  repoName: repoName || '',
36
50
  repoPath,
@@ -70,6 +84,7 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName,
70
84
  }
71
85
  }, IDLE_TIMEOUT_MS);
72
86
  }
87
+ const continueArgs = AGENT_CONTINUE_ARGS[agent];
73
88
  function attachHandlers(proc, canRetry) {
74
89
  const spawnTime = Date.now();
75
90
  proc.onData((data) => {
@@ -89,12 +104,12 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName,
89
104
  }
90
105
  });
91
106
  proc.onExit(({ exitCode }) => {
92
- // If --continue failed quickly, retry without it
107
+ // If continue args failed quickly, retry without them
93
108
  if (canRetry && (Date.now() - spawnTime) < 3000 && exitCode !== 0) {
94
- const retryArgs = args.filter(a => a !== '--continue');
109
+ const retryArgs = args.filter(a => !continueArgs.includes(a));
95
110
  scrollback.length = 0;
96
111
  scrollbackBytes = 0;
97
- const retryPty = pty.spawn(command, retryArgs, {
112
+ const retryPty = pty.spawn(resolvedCommand, retryArgs, {
98
113
  name: 'xterm-256color',
99
114
  cols,
100
115
  rows,
@@ -117,17 +132,18 @@ function create({ type, repoName, repoPath, cwd, root, worktreeName, branchName,
117
132
  fs.rm(tmpDir, { recursive: true, force: true }, () => { });
118
133
  });
119
134
  }
120
- attachHandlers(ptyProcess, args.includes('--continue'));
121
- return { id, type: session.type, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, branchName: session.branchName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false };
135
+ attachHandlers(ptyProcess, continueArgs.some(a => args.includes(a)));
136
+ return { id, type: session.type, agent: session.agent, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, branchName: session.branchName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false };
122
137
  }
123
138
  function get(id) {
124
139
  return sessions.get(id);
125
140
  }
126
141
  function list() {
127
142
  return Array.from(sessions.values())
128
- .map(({ id, type, root, repoName, repoPath, worktreeName, branchName, displayName, createdAt, lastActivity, idle }) => ({
143
+ .map(({ id, type, agent, root, repoName, repoPath, worktreeName, branchName, displayName, createdAt, lastActivity, idle }) => ({
129
144
  id,
130
145
  type,
146
+ agent,
131
147
  root,
132
148
  repoName,
133
149
  repoPath,
@@ -172,4 +188,4 @@ function write(id, data) {
172
188
  function findRepoSession(repoPath) {
173
189
  return list().find((s) => s.type === 'repo' && s.repoPath === repoPath);
174
190
  }
175
- export { create, get, list, kill, resize, updateDisplayName, write, onIdleChange, findRepoSession };
191
+ export { create, get, list, kill, resize, updateDisplayName, write, onIdleChange, findRepoSession, AGENT_COMMANDS, AGENT_YOLO_ARGS, AGENT_CONTINUE_ARGS };
@@ -8,6 +8,32 @@ export function isValidWorktreePath(worktreePath) {
8
8
  return resolved.includes(path.sep + dir + path.sep);
9
9
  });
10
10
  }
11
+ /**
12
+ * Parse `git worktree list --porcelain` output into ALL entries (including main worktree).
13
+ * Skips bare entries. Detached HEAD entries get empty branch string.
14
+ */
15
+ export function parseAllWorktrees(stdout, repoPath) {
16
+ const results = [];
17
+ const blocks = stdout.split('\n\n').filter(Boolean);
18
+ for (const block of blocks) {
19
+ const lines = block.split('\n');
20
+ let wtPath = '';
21
+ let branch = '';
22
+ let bare = false;
23
+ for (const line of lines) {
24
+ if (line.startsWith('worktree '))
25
+ wtPath = line.slice(9);
26
+ if (line.startsWith('branch refs/heads/'))
27
+ branch = line.slice(18);
28
+ if (line === 'bare')
29
+ bare = true;
30
+ }
31
+ if (!wtPath || bare)
32
+ continue;
33
+ results.push({ path: wtPath, branch, isMain: wtPath === repoPath });
34
+ }
35
+ return results;
36
+ }
11
37
  /**
12
38
  * Parse `git worktree list --porcelain` output into structured entries.
13
39
  * Skips the main worktree (matching repoPath) and bare/detached entries.
@@ -40,6 +40,7 @@ test('loadConfig merges with defaults for missing fields', () => {
40
40
  assert.deepEqual(config.repos, DEFAULTS.repos);
41
41
  assert.equal(config.claudeCommand, DEFAULTS.claudeCommand);
42
42
  assert.deepEqual(config.claudeArgs, DEFAULTS.claudeArgs);
43
+ assert.equal(config.defaultAgent, DEFAULTS.defaultAgent);
43
44
  });
44
45
  test('loadConfig throws if config file not found', () => {
45
46
  const configPath = path.join(tmpDir, 'nonexistent.json');
@@ -59,6 +60,7 @@ test('DEFAULTS has expected keys and values', () => {
59
60
  assert.deepEqual(DEFAULTS.repos, []);
60
61
  assert.equal(DEFAULTS.claudeCommand, 'claude');
61
62
  assert.deepEqual(DEFAULTS.claudeArgs, []);
63
+ assert.equal(DEFAULTS.defaultAgent, 'claude');
62
64
  });
63
65
  test('ensureMetaDir creates worktree-meta directory', () => {
64
66
  const configPath = path.join(tmpDir, 'config.json');
@@ -264,4 +264,39 @@ describe('sessions', () => {
264
264
  createdIds.push(result.id);
265
265
  assert.strictEqual(result.branchName, '');
266
266
  });
267
+ it('agent defaults to claude when not specified', () => {
268
+ const result = sessions.create({
269
+ repoName: 'test-repo',
270
+ repoPath: '/tmp',
271
+ command: '/bin/echo',
272
+ args: ['hello'],
273
+ });
274
+ createdIds.push(result.id);
275
+ assert.strictEqual(result.agent, 'claude');
276
+ });
277
+ it('agent is set when specified', () => {
278
+ const result = sessions.create({
279
+ repoName: 'test-repo',
280
+ repoPath: '/tmp',
281
+ agent: 'codex',
282
+ command: '/bin/echo',
283
+ args: ['hello'],
284
+ });
285
+ createdIds.push(result.id);
286
+ assert.strictEqual(result.agent, 'codex');
287
+ });
288
+ it('list includes agent field', () => {
289
+ const result = sessions.create({
290
+ repoName: 'test-repo',
291
+ repoPath: '/tmp',
292
+ agent: 'codex',
293
+ command: '/bin/echo',
294
+ args: ['hello'],
295
+ });
296
+ createdIds.push(result.id);
297
+ const list = sessions.list();
298
+ const session = list.find(s => s.id === result.id);
299
+ assert.ok(session);
300
+ assert.strictEqual(session.agent, 'codex');
301
+ });
267
302
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain } from '../server/watcher.js';
3
+ import { WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain, parseAllWorktrees } from '../server/watcher.js';
4
4
  describe('worktree directories constant', () => {
5
5
  it('should include both .worktrees and .claude/worktrees', () => {
6
6
  assert.deepEqual(WORKTREE_DIRS, ['.worktrees', '.claude/worktrees']);
@@ -146,6 +146,91 @@ describe('parseWorktreeListPorcelain', () => {
146
146
  assert.equal(result[0].branch, 'dy/feat/deep/nesting/here');
147
147
  });
148
148
  });
149
+ describe('parseAllWorktrees', () => {
150
+ const repoPath = '/Users/me/code/my-repo';
151
+ it('should include the main worktree with isMain=true', () => {
152
+ const stdout = [
153
+ `worktree ${repoPath}`,
154
+ 'HEAD abc123',
155
+ 'branch refs/heads/main',
156
+ '',
157
+ ].join('\n');
158
+ const result = parseAllWorktrees(stdout, repoPath);
159
+ assert.equal(result.length, 1);
160
+ assert.equal(result[0].path, repoPath);
161
+ assert.equal(result[0].branch, 'main');
162
+ assert.equal(result[0].isMain, true);
163
+ });
164
+ it('should mark non-main worktrees with isMain=false', () => {
165
+ const stdout = [
166
+ `worktree ${repoPath}`,
167
+ 'HEAD abc123',
168
+ 'branch refs/heads/main',
169
+ '',
170
+ 'worktree /Users/me/code/my-repo/.worktrees/feat-branch',
171
+ 'HEAD def456',
172
+ 'branch refs/heads/feat/branch',
173
+ '',
174
+ ].join('\n');
175
+ const result = parseAllWorktrees(stdout, repoPath);
176
+ assert.equal(result.length, 2);
177
+ assert.equal(result[0].isMain, true);
178
+ assert.equal(result[1].isMain, false);
179
+ assert.equal(result[1].path, '/Users/me/code/my-repo/.worktrees/feat-branch');
180
+ assert.equal(result[1].branch, 'feat/branch');
181
+ });
182
+ it('should still skip bare entries', () => {
183
+ const stdout = [
184
+ `worktree ${repoPath}`,
185
+ 'HEAD abc123',
186
+ 'branch refs/heads/main',
187
+ '',
188
+ 'worktree /some/bare/repo',
189
+ 'HEAD def456',
190
+ 'bare',
191
+ '',
192
+ ].join('\n');
193
+ const result = parseAllWorktrees(stdout, repoPath);
194
+ assert.equal(result.length, 1);
195
+ assert.equal(result[0].isMain, true);
196
+ });
197
+ it('should include detached HEAD entries with empty branch', () => {
198
+ const stdout = [
199
+ `worktree ${repoPath}`,
200
+ 'HEAD abc123',
201
+ 'branch refs/heads/main',
202
+ '',
203
+ 'worktree /Users/me/code/my-repo/.worktrees/detached',
204
+ 'HEAD def456',
205
+ 'detached',
206
+ '',
207
+ ].join('\n');
208
+ const result = parseAllWorktrees(stdout, repoPath);
209
+ assert.equal(result.length, 2);
210
+ assert.equal(result[1].branch, '');
211
+ });
212
+ it('should handle empty output', () => {
213
+ const result = parseAllWorktrees('', repoPath);
214
+ assert.equal(result.length, 0);
215
+ });
216
+ it('should find worktree by branch name', () => {
217
+ const stdout = [
218
+ `worktree ${repoPath}`,
219
+ 'HEAD abc123',
220
+ 'branch refs/heads/dy/feat/worktree-isolation',
221
+ '',
222
+ 'worktree /Users/me/code/my-repo/.worktrees/feat-a',
223
+ 'HEAD def456',
224
+ 'branch refs/heads/feat/a',
225
+ '',
226
+ ].join('\n');
227
+ const result = parseAllWorktrees(stdout, repoPath);
228
+ const match = result.find(wt => wt.branch === 'dy/feat/worktree-isolation');
229
+ assert.ok(match);
230
+ assert.equal(match.path, repoPath);
231
+ assert.equal(match.isMain, true);
232
+ });
233
+ });
149
234
  describe('CLI worktree arg parsing', () => {
150
235
  it('should extract --yolo and leave other args intact', () => {
151
236
  const args = ['add', './.worktrees/my-feature', '-b', 'my-feature', '--yolo'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",