claude-remote-cli 3.5.2 → 3.5.4

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-DnrS1YFL.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-D8zGa-Fd.css">
14
+ <script type="module" crossorigin src="/assets/index-QZrLSCSL.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-CiwYPknn.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
@@ -74,6 +74,28 @@ export function getWorkspaceSettings(config, workspacePath) {
74
74
  // Per-workspace settings override global — only for defined keys
75
75
  return { ...globalDefaults, ...perWorkspace };
76
76
  }
77
+ export function resolveSessionSettings(config, repoPath, overrides) {
78
+ const ws = getWorkspaceSettings(config, repoPath);
79
+ return {
80
+ agent: overrides.agent ?? ws.defaultAgent ?? 'claude',
81
+ yolo: overrides.yolo ?? ws.defaultYolo ?? false,
82
+ continue: overrides.continue ?? ws.defaultContinue ?? true,
83
+ useTmux: overrides.useTmux ?? ws.launchInTmux ?? false,
84
+ claudeArgs: overrides.claudeArgs ?? ws.claudeArgs ?? [],
85
+ };
86
+ }
87
+ export function deleteWorkspaceSettingKeys(configPath, config, workspacePath, keys) {
88
+ if (!config.workspaceSettings?.[workspacePath])
89
+ return;
90
+ for (const key of keys) {
91
+ delete config.workspaceSettings[workspacePath][key];
92
+ }
93
+ // Clean up empty workspace entries
94
+ if (Object.keys(config.workspaceSettings[workspacePath]).length === 0) {
95
+ delete config.workspaceSettings[workspacePath];
96
+ }
97
+ saveConfig(configPath, config);
98
+ }
77
99
  export function setWorkspaceSettings(configPath, config, workspacePath, settings) {
78
100
  if (!config.workspaceSettings)
79
101
  config.workspaceSettings = {};
@@ -8,7 +8,7 @@ import { execFile } from 'node:child_process';
8
8
  import { promisify } from 'node:util';
9
9
  import express from 'express';
10
10
  import cookieParser from 'cookie-parser';
11
- import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, deleteMeta, ensureMetaDir } from './config.js';
11
+ import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, deleteMeta, ensureMetaDir, resolveSessionSettings } from './config.js';
12
12
  import * as auth from './auth.js';
13
13
  import * as sessions from './sessions.js';
14
14
  import { AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS, serializeAll, restoreFromDisk, activeTmuxSessionNames, populateMetaCache } from './sessions.js';
@@ -542,12 +542,12 @@ async function main() {
542
542
  // Sanitize optional terminal dimensions
543
543
  const safeCols = typeof cols === 'number' && Number.isFinite(cols) && cols >= 1 && cols <= 500 ? Math.round(cols) : undefined;
544
544
  const safeRows = typeof rows === 'number' && Number.isFinite(rows) && rows >= 1 && rows <= 200 ? Math.round(rows) : undefined;
545
- const resolvedAgent = agent || config.defaultAgent || 'claude';
545
+ const resolved = resolveSessionSettings(config, repoPath, { agent, yolo, useTmux, claudeArgs });
546
+ const resolvedAgent = resolved.agent;
546
547
  const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
547
548
  const baseArgs = [
548
- ...(config.claudeArgs || []),
549
- ...(yolo ? AGENT_YOLO_ARGS[resolvedAgent] : []),
550
- ...(claudeArgs || []),
549
+ ...(resolved.claudeArgs),
550
+ ...(resolved.yolo ? AGENT_YOLO_ARGS[resolvedAgent] : []),
551
551
  ];
552
552
  // Compute root by matching repoPath against configured rootDirs
553
553
  const roots = config.rootDirs || [];
@@ -653,7 +653,7 @@ async function main() {
653
653
  root,
654
654
  displayName: name,
655
655
  args: baseArgs,
656
- useTmux: useTmux ?? config.launchInTmux,
656
+ useTmux: resolved.useTmux,
657
657
  ...(safeCols != null && { cols: safeCols }),
658
658
  ...(safeRows != null && { rows: safeRows }),
659
659
  });
@@ -679,7 +679,7 @@ async function main() {
679
679
  displayName: displayNameVal,
680
680
  args,
681
681
  configPath: CONFIG_PATH,
682
- useTmux: useTmux ?? config.launchInTmux,
682
+ useTmux: resolved.useTmux,
683
683
  ...(safeCols != null && { cols: safeCols }),
684
684
  ...(safeRows != null && { rows: safeRows }),
685
685
  });
@@ -724,7 +724,7 @@ async function main() {
724
724
  displayName,
725
725
  args,
726
726
  configPath: CONFIG_PATH,
727
- useTmux: useTmux ?? config.launchInTmux,
727
+ useTmux: resolved.useTmux,
728
728
  ...(safeCols != null && { cols: safeCols }),
729
729
  ...(safeRows != null && { rows: safeRows }),
730
730
  needsBranchRename: isMountainName || (needsBranchRename ?? false),
@@ -747,18 +747,20 @@ async function main() {
747
747
  res.status(400).json({ error: 'repoPath is required' });
748
748
  return;
749
749
  }
750
- const resolvedAgent = agent || config.defaultAgent || 'claude';
750
+ const resolved = resolveSessionSettings(config, repoPath, {
751
+ agent, yolo, continue: continueSession, useTmux, claudeArgs,
752
+ });
753
+ const resolvedAgent = resolved.agent;
751
754
  // Sanitize optional terminal dimensions
752
755
  const safeCols = typeof cols === 'number' && Number.isFinite(cols) && cols >= 1 && cols <= 500 ? Math.round(cols) : undefined;
753
756
  const safeRows = typeof rows === 'number' && Number.isFinite(rows) && rows >= 1 && rows <= 200 ? Math.round(rows) : undefined;
754
757
  // Multiple sessions per repo allowed (multi-tab support)
755
758
  const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
756
759
  const baseArgs = [
757
- ...(config.claudeArgs || []),
758
- ...(yolo ? AGENT_YOLO_ARGS[resolvedAgent] : []),
759
- ...(claudeArgs || []),
760
+ ...(resolved.claudeArgs),
761
+ ...(resolved.yolo ? AGENT_YOLO_ARGS[resolvedAgent] : []),
760
762
  ];
761
- const args = continueSession ? [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs] : [...baseArgs];
763
+ const args = resolved.continue ? [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs] : [...baseArgs];
762
764
  const roots = config.rootDirs || [];
763
765
  const root = roots.find(function (r) { return repoPath.startsWith(r); }) || '';
764
766
  let branchName = '';
@@ -777,7 +779,7 @@ async function main() {
777
779
  displayName: name,
778
780
  args,
779
781
  branchName,
780
- useTmux: useTmux ?? config.launchInTmux,
782
+ useTmux: resolved.useTmux,
781
783
  ...(safeCols != null && { cols: safeCols }),
782
784
  ...(safeRows != null && { rows: safeRows }),
783
785
  });
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
  import { Router } from 'express';
7
- import { loadConfig, saveConfig, getWorkspaceSettings, setWorkspaceSettings } from './config.js';
7
+ import { loadConfig, saveConfig, getWorkspaceSettings, setWorkspaceSettings, deleteWorkspaceSettingKeys } from './config.js';
8
8
  import { listBranches, getActivityFeed, getCiStatus, getPrForBranch, getUnresolvedCommentCount, switchBranch, getCurrentBranch } from './git.js';
9
9
  import { MOUNTAIN_NAMES } from './types.js';
10
10
  const execFileAsync = promisify(execFile);
@@ -341,14 +341,27 @@ export function createWorkspaceRouter(deps) {
341
341
  // -------------------------------------------------------------------------
342
342
  router.get('/settings', async (req, res) => {
343
343
  const workspacePath = typeof req.query.path === 'string' ? req.query.path : undefined;
344
+ const merged = req.query.merged === 'true';
344
345
  if (!workspacePath) {
345
346
  res.status(400).json({ error: 'path query parameter is required' });
346
347
  return;
347
348
  }
348
349
  const config = getConfig();
349
350
  const resolved = path.resolve(workspacePath);
350
- const settings = config.workspaceSettings?.[resolved] ?? {};
351
- res.json(settings);
351
+ if (merged) {
352
+ const wsOverrides = config.workspaceSettings?.[resolved] ?? {};
353
+ const effective = getWorkspaceSettings(config, resolved);
354
+ const overridden = [];
355
+ for (const key of ['defaultAgent', 'defaultContinue', 'defaultYolo', 'launchInTmux']) {
356
+ if (wsOverrides[key] !== undefined)
357
+ overridden.push(key);
358
+ }
359
+ res.json({ settings: effective, overridden });
360
+ }
361
+ else {
362
+ const settings = config.workspaceSettings?.[resolved] ?? {};
363
+ res.json(settings);
364
+ }
352
365
  });
353
366
  // -------------------------------------------------------------------------
354
367
  // PATCH /workspaces/settings — update per-workspace settings
@@ -362,11 +375,28 @@ export function createWorkspaceRouter(deps) {
362
375
  const resolved = path.resolve(workspacePath);
363
376
  const updates = req.body;
364
377
  const config = getConfig();
365
- const current = config.workspaceSettings?.[resolved] ?? {};
366
- const merged = { ...current, ...updates };
367
- config.workspaceSettings = { ...config.workspaceSettings, [resolved]: merged };
368
- saveConfig(configPath, config);
369
- res.json(merged);
378
+ // Separate null values (deletions) from actual updates
379
+ const keysToDelete = [];
380
+ const keysToUpdate = {};
381
+ for (const [key, value] of Object.entries(updates)) {
382
+ if (value === null) {
383
+ keysToDelete.push(key);
384
+ }
385
+ else {
386
+ keysToUpdate[key] = value;
387
+ }
388
+ }
389
+ // Apply deletions first
390
+ if (keysToDelete.length > 0) {
391
+ deleteWorkspaceSettingKeys(configPath, config, resolved, keysToDelete);
392
+ }
393
+ // Apply updates
394
+ if (Object.keys(keysToUpdate).length > 0) {
395
+ setWorkspaceSettings(configPath, config, resolved, keysToUpdate);
396
+ }
397
+ // Return the current raw workspace settings
398
+ const final = config.workspaceSettings?.[resolved] ?? {};
399
+ res.json(final);
370
400
  });
371
401
  // -------------------------------------------------------------------------
372
402
  // GET /workspaces/pr — PR info for a specific branch
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import os from 'node:os';
6
- import { DEFAULTS, loadConfig, saveConfig, ensureMetaDir, readMeta, writeMeta, deleteMeta } from '../server/config.js';
6
+ import { DEFAULTS, loadConfig, saveConfig, ensureMetaDir, readMeta, writeMeta, deleteMeta, resolveSessionSettings, deleteWorkspaceSettingKeys } from '../server/config.js';
7
7
  let tmpDir;
8
8
  before(() => {
9
9
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-remote-cli-config-test-'));
@@ -110,3 +110,116 @@ test('deleteMeta is a no-op for non-existent metadata', () => {
110
110
  const configPath = path.join(tmpDir, 'config.json');
111
111
  assert.doesNotThrow(() => deleteMeta(configPath, '/no/such/path'));
112
112
  });
113
+ test('resolveSessionSettings returns global defaults when no workspace or overrides', () => {
114
+ const configPath = path.join(tmpDir, 'config.json');
115
+ fs.writeFileSync(configPath, JSON.stringify({
116
+ defaultAgent: 'claude',
117
+ defaultContinue: true,
118
+ defaultYolo: false,
119
+ launchInTmux: false,
120
+ claudeArgs: [],
121
+ }), 'utf8');
122
+ const config = loadConfig(configPath);
123
+ const result = resolveSessionSettings(config, '/some/repo', {});
124
+ assert.equal(result.agent, 'claude');
125
+ assert.equal(result.yolo, false);
126
+ assert.equal(result.continue, true);
127
+ assert.equal(result.useTmux, false);
128
+ assert.deepEqual(result.claudeArgs, []);
129
+ });
130
+ test('resolveSessionSettings applies workspace overrides over globals', () => {
131
+ const configPath = path.join(tmpDir, 'config.json');
132
+ fs.writeFileSync(configPath, JSON.stringify({
133
+ defaultAgent: 'claude',
134
+ defaultYolo: false,
135
+ defaultContinue: true,
136
+ launchInTmux: false,
137
+ claudeArgs: [],
138
+ workspaceSettings: {
139
+ '/my/repo': { defaultYolo: true, defaultAgent: 'codex' },
140
+ },
141
+ }), 'utf8');
142
+ const config = loadConfig(configPath);
143
+ const result = resolveSessionSettings(config, '/my/repo', {});
144
+ assert.equal(result.agent, 'codex');
145
+ assert.equal(result.yolo, true);
146
+ assert.equal(result.continue, true);
147
+ });
148
+ test('resolveSessionSettings explicit overrides beat workspace settings', () => {
149
+ const configPath = path.join(tmpDir, 'config.json');
150
+ fs.writeFileSync(configPath, JSON.stringify({
151
+ defaultAgent: 'claude',
152
+ defaultYolo: true,
153
+ defaultContinue: true,
154
+ launchInTmux: false,
155
+ claudeArgs: [],
156
+ workspaceSettings: {
157
+ '/my/repo': { defaultYolo: true },
158
+ },
159
+ }), 'utf8');
160
+ const config = loadConfig(configPath);
161
+ const result = resolveSessionSettings(config, '/my/repo', { yolo: false });
162
+ assert.equal(result.yolo, false);
163
+ });
164
+ test('resolveSessionSettings uses override claudeArgs, not global', () => {
165
+ const configPath = path.join(tmpDir, 'config.json');
166
+ fs.writeFileSync(configPath, JSON.stringify({
167
+ defaultAgent: 'claude',
168
+ defaultYolo: false,
169
+ defaultContinue: true,
170
+ launchInTmux: false,
171
+ claudeArgs: ['--global-arg'],
172
+ }), 'utf8');
173
+ const config = loadConfig(configPath);
174
+ const result = resolveSessionSettings(config, '/some/repo', { claudeArgs: ['--custom'] });
175
+ assert.deepEqual(result.claudeArgs, ['--custom']);
176
+ });
177
+ test('resolveSessionSettings falls through to globals when no workspace exists', () => {
178
+ const configPath = path.join(tmpDir, 'config.json');
179
+ fs.writeFileSync(configPath, JSON.stringify({
180
+ defaultAgent: 'codex',
181
+ defaultYolo: true,
182
+ defaultContinue: false,
183
+ launchInTmux: true,
184
+ claudeArgs: ['--verbose'],
185
+ }), 'utf8');
186
+ const config = loadConfig(configPath);
187
+ const result = resolveSessionSettings(config, '/nonexistent/repo', {});
188
+ assert.equal(result.agent, 'codex');
189
+ assert.equal(result.yolo, true);
190
+ assert.equal(result.continue, false);
191
+ assert.equal(result.useTmux, true);
192
+ assert.deepEqual(result.claudeArgs, ['--verbose']);
193
+ });
194
+ test('deleteWorkspaceSettingKeys removes specified keys', () => {
195
+ const configPath = path.join(tmpDir, 'config.json');
196
+ const config = {
197
+ ...DEFAULTS,
198
+ workspaceSettings: {
199
+ '/my/repo': { defaultYolo: true, defaultAgent: 'codex', branchPrefix: 'dy/' },
200
+ },
201
+ };
202
+ fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
203
+ deleteWorkspaceSettingKeys(configPath, config, '/my/repo', ['defaultYolo', 'defaultAgent']);
204
+ assert.equal(config.workspaceSettings['/my/repo'].defaultYolo, undefined);
205
+ assert.equal(config.workspaceSettings['/my/repo'].defaultAgent, undefined);
206
+ assert.equal(config.workspaceSettings['/my/repo'].branchPrefix, 'dy/');
207
+ });
208
+ test('deleteWorkspaceSettingKeys removes entire workspace entry when empty', () => {
209
+ const configPath = path.join(tmpDir, 'config.json');
210
+ const config = {
211
+ ...DEFAULTS,
212
+ workspaceSettings: {
213
+ '/my/repo': { defaultYolo: true },
214
+ },
215
+ };
216
+ fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
217
+ deleteWorkspaceSettingKeys(configPath, config, '/my/repo', ['defaultYolo']);
218
+ assert.equal(config.workspaceSettings['/my/repo'], undefined);
219
+ });
220
+ test('deleteWorkspaceSettingKeys is no-op for nonexistent workspace', () => {
221
+ const configPath = path.join(tmpDir, 'config.json');
222
+ const config = { ...DEFAULTS };
223
+ fs.writeFileSync(configPath, JSON.stringify(config), 'utf8');
224
+ assert.doesNotThrow(() => deleteWorkspaceSettingKeys(configPath, config, '/no/such/repo', ['defaultYolo']));
225
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",