Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"bump": "node scripts/sync-version.js --bump",
"bump:minor": "node scripts/sync-version.js --bump --minor",
"bump:major": "node scripts/sync-version.js --bump --major",
"build": "tsc && shx cp setup-claude-server.js uninstall-claude-server.js track-installation.js dist/ && shx chmod +x dist/*.js && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && shx mkdir -p dist/remote-device/scripts && shx cp src/remote-device/scripts/blocking-offline-update.js dist/remote-device/scripts/ && node scripts/build-ui-runtime.cjs file-preview",
"build": "tsc && shx cp setup-claude-server.js uninstall-claude-server.js track-installation.js dist/ && shx chmod +x dist/*.js && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && shx mkdir -p dist/remote-device/scripts && shx cp src/remote-device/scripts/blocking-offline-update.js dist/remote-device/scripts/ && node scripts/build-ui-runtime.cjs",
"watch": "tsc --watch",
"start": "node dist/index.js",
"start:debug": "node --inspect-brk=9229 dist/index.js",
Expand Down
11 changes: 11 additions & 0 deletions scripts/build-ui-runtime.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,20 @@ const TARGETS = {
staticDir: 'src/ui/file-preview',
styleLayers: [
'src/ui/styles/base.css',
'src/ui/styles/components/compact-row.css',
'src/ui/styles/components/tool-header.css',
'src/ui/styles/apps/file-preview.css'
]
},
'config-editor': {
entry: 'src/ui/config-editor/src/main.ts',
output: 'dist/ui/config-editor/config-editor-runtime.js',
staticDir: 'src/ui/config-editor',
styleLayers: [
'src/ui/styles/base.css',
'src/ui/styles/components/compact-row.css',
'src/ui/styles/apps/config-editor.css'
]
}
};

Expand Down
49 changes: 49 additions & 0 deletions src/config-field-definitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export type ConfigFieldValueType = 'string' | 'number' | 'boolean' | 'array' | 'null';

export type ConfigFieldDefinition = {
label: string;
description: string;
valueType: ConfigFieldValueType;
};

// Single source of truth for user-editable configuration fields.
export const CONFIG_FIELD_DEFINITIONS = {
blockedCommands: {
label: 'Blocked Commands',
description: 'This is your personal safety blocklist. If a command appears here, Desktop Commander will refuse to run it even if a prompt asks for it. Add risky commands you never want executed by mistake.',
valueType: 'array',
},
allowedDirectories: {
label: 'Allowed Folders',
description: 'These are the folders Desktop Commander is allowed to read and edit. Think of this as a permission list. Keeping it small is safer. If this list is empty, Desktop Commander can access your entire filesystem.',
valueType: 'array',
},
defaultShell: {
label: 'Default Shell',
description: 'This is the shell used for new command sessions (for example /bin/bash or /bin/zsh). Only change this if you know your environment requires a specific shell.',
valueType: 'string',
},
telemetryEnabled: {
label: 'Anonymous Telemetry',
description: 'When on, Desktop Commander sends anonymous usage information that helps improve product quality. When off, no telemetry data is sent.',
valueType: 'boolean',
},
fileReadLineLimit: {
label: 'File Read Limit',
description: 'Maximum number of lines returned from a file in one read action. Lower numbers keep responses short and safer; higher numbers return more text at once.',
valueType: 'number',
},
fileWriteLineLimit: {
label: 'File Write Limit',
description: 'Maximum number of lines that can be written in one edit operation. This helps prevent accidental oversized writes and keeps file changes predictable.',
valueType: 'number',
},
} as const satisfies Record<string, ConfigFieldDefinition>;

export type ConfigFieldKey = keyof typeof CONFIG_FIELD_DEFINITIONS;

export const CONFIG_FIELD_KEYS = Object.keys(CONFIG_FIELD_DEFINITIONS) as ConfigFieldKey[];

export function isConfigFieldKey(value: string): value is ConfigFieldKey {
return Object.prototype.hasOwnProperty.call(CONFIG_FIELD_DEFINITIONS, value);
}
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { capture, capture_call_tool } from "./utils/capture.js";
import { logToStderr, logger } from './utils/logger.js';
import {
buildUiToolMeta,
CONFIG_EDITOR_RESOURCE_URI,
FILE_PREVIEW_RESOURCE_URI
} from './ui/contracts.js';
import { listUiResources, readUiResource } from './ui/resources.js';
Expand Down Expand Up @@ -240,6 +241,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
- systemInfo (operating system and environment details)
${CMD_PREFIX_DESCRIPTION}`,
inputSchema: zodToJsonSchema(GetConfigArgsSchema),
_meta: buildUiToolMeta(CONFIG_EDITOR_RESOURCE_URI, true),
annotations: {
title: "Get Configuration",
readOnlyHint: true,
Expand Down
117 changes: 101 additions & 16 deletions src/tools/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,86 @@ import { SetConfigValueArgsSchema } from './schemas.js';
import { getSystemInfo } from '../utils/system-info.js';
import { currentClient } from '../server.js';
import { featureFlagManager } from '../utils/feature-flags.js';
import { access, readFile } from 'node:fs/promises';
import { constants as fsConstants } from 'node:fs';
import {
CONFIG_FIELD_DEFINITIONS,
CONFIG_FIELD_KEYS,
isConfigFieldKey,
} from '../config-field-definitions.js';

const ALLOWED_CONFIG_KEYS = new Set(CONFIG_FIELD_KEYS);

async function pathExists(pathValue: string): Promise<boolean> {
try {
await access(pathValue, fsConstants.X_OK);
return true;
} catch {
return false;
}
}

async function detectAvailableShells(systemInfo: ReturnType<typeof getSystemInfo>): Promise<string[]> {
const detected = new Set<string>();
const add = (shell: string): void => {
if (shell.trim().length > 0) {
detected.add(shell.trim());
}
};

add(systemInfo.defaultShell);

if (systemInfo.isWindows) {
add(process.env.ComSpec ?? '');
const systemRoot = process.env.SystemRoot ?? 'C:\\Windows';
const candidates = [
`${systemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
`${systemRoot}\\System32\\cmd.exe`,
`${systemRoot}\\System32\\bash.exe`,
'powershell.exe',
'pwsh.exe',
'cmd.exe',
'bash.exe',
];

for (const shell of candidates) {
if (shell.includes('\\')) {
if (await pathExists(shell)) {
add(shell);
}
} else {
add(shell);
}
}

return [...detected];
}

add(process.env.SHELL ?? '');

const shellFiles = ['/etc/shells'];
for (const shellFile of shellFiles) {
try {
const content = await readFile(shellFile, 'utf8');
content
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith('#'))
.forEach(add);
} catch {
// Best-effort discovery only.
}
}

const fallbackCandidates = ['/bin/zsh', '/bin/bash', '/bin/sh', '/usr/bin/fish'];
for (const shell of fallbackCandidates) {
if (await pathExists(shell)) {
add(shell);
}
}

return [...detected];
}

/**
* Get the entire config including system information
Expand Down Expand Up @@ -34,13 +114,30 @@ export async function getConfig() {
memory
}
};
const availableShells = await detectAvailableShells(systemInfo);

console.error(`getConfig result: ${JSON.stringify(configWithSystemInfo, null, 2)}`);
return {
content: [{
type: "text",
text: `Current configuration:\n${JSON.stringify(configWithSystemInfo, null, 2)}`
}],
structuredContent: {
config: configWithSystemInfo,
uiHints: {
availableShells,
},
entries: CONFIG_FIELD_KEYS.map((key) => {
const definition = CONFIG_FIELD_DEFINITIONS[key];
const value = (configWithSystemInfo as Record<string, unknown>)[key];
return {
key,
value,
valueType: definition.valueType,
editable: true,
};
}),
},
};
} catch (error) {
console.error(`Error in getConfig: ${error instanceof Error ? error.message : String(error)}`);
Expand All @@ -55,18 +152,6 @@ export async function getConfig() {
}
}

// Keys that can be set via the set_config_value MCP tool.
// Internal keys (clientId, usageStats, abTest_*, onboardingState, etc.)
// are managed by the server itself and should not be settable by clients.
const ALLOWED_CONFIG_KEYS = new Set([
'blockedCommands',
'allowedDirectories',
'defaultShell',
'telemetryEnabled',
'fileReadLineLimit',
'fileWriteLineLimit',
]);

/**
* Set a specific config value
*/
Expand All @@ -85,7 +170,7 @@ export async function setConfigValue(args: unknown) {
};
}

if (!ALLOWED_CONFIG_KEYS.has(parsed.data.key)) {
if (!isConfigFieldKey(parsed.data.key)) {
return {
content: [{
type: "text",
Expand All @@ -96,6 +181,7 @@ export async function setConfigValue(args: unknown) {
}

try {
const fieldDefinition = CONFIG_FIELD_DEFINITIONS[parsed.data.key];
// Parse string values that should be arrays or objects
let valueToStore = parsed.data.value;

Expand All @@ -111,8 +197,7 @@ export async function setConfigValue(args: unknown) {
}

// Special handling for known array configuration keys
if ((parsed.data.key === 'allowedDirectories' || parsed.data.key === 'blockedCommands') &&
!Array.isArray(valueToStore)) {
if (fieldDefinition.valueType === 'array' && !Array.isArray(valueToStore)) {
if (typeof valueToStore === 'string') {
const originalString = valueToStore;
try {
Expand Down Expand Up @@ -169,4 +254,4 @@ export async function setConfigValue(args: unknown) {
isError: true
};
}
}
}
13 changes: 13 additions & 0 deletions src/ui/config-editor/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Desktop Commander Config Editor</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div id="app"></div>
<script src="./config-editor-runtime.js"></script>
</body>
</html>
Loading