@mokoconsulting/mcp-windows 3.0.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/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogitea/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogitea/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogitea/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogitea/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_connection_issue.md +67 -0
- package/.mokogitea/ISSUE_TEMPLATE/mcp_tool_request.md +49 -0
- package/.mokogitea/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogitea/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogitea/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogitea/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogitea/branch-protection.yml +251 -0
- package/.mokogitea/workflows/auto-assign.yml +76 -0
- package/.mokogitea/workflows/auto-bump.yml +66 -0
- package/.mokogitea/workflows/auto-dev-issue.yml +207 -0
- package/.mokogitea/workflows/auto-release.yml +421 -0
- package/.mokogitea/workflows/branch-cleanup.yml +48 -0
- package/.mokogitea/workflows/cascade-dev.yml +10 -0
- package/.mokogitea/workflows/changelog-validation.yml +101 -0
- package/.mokogitea/workflows/ci-generic.yml +191 -0
- package/.mokogitea/workflows/cleanup.yml +87 -0
- package/.mokogitea/workflows/codeql-analysis.yml +115 -0
- package/.mokogitea/workflows/copilot-agent.yml +44 -0
- package/.mokogitea/workflows/deploy-manual.yml +126 -0
- package/.mokogitea/workflows/enterprise-firewall-setup.yml +758 -0
- package/.mokogitea/workflows/gitleaks.yml +92 -0
- package/.mokogitea/workflows/issue-branch.yml +73 -0
- package/.mokogitea/workflows/mcp-auto-release.yml +278 -0
- package/.mokogitea/workflows/mcp-build-test.yml +65 -0
- package/.mokogitea/workflows/mcp-sdk-check.yml +109 -0
- package/.mokogitea/workflows/mcp-tool-inventory.yml +61 -0
- package/.mokogitea/workflows/notify.yml +70 -0
- package/.mokogitea/workflows/npm-publish.yml +113 -0
- package/.mokogitea/workflows/pr-check.yml +534 -0
- package/.mokogitea/workflows/pre-release.yml +252 -0
- package/.mokogitea/workflows/rc-revert.yml +66 -0
- package/.mokogitea/workflows/repo-health.yml +712 -0
- package/.mokogitea/workflows/repository-cleanup.yml +525 -0
- package/.mokogitea/workflows/security-audit.yml +82 -0
- package/.mokogitea/workflows/standards-compliance.yml +2614 -0
- package/.mokogitea/workflows/sync-version-on-merge.yml +133 -0
- package/.mokogitea/workflows/update-server.yml +312 -0
- package/.mokogitea/workflows/workflow-sync-trigger.yml +73 -0
- package/CHANGELOG.md +130 -0
- package/CLAUDE.md +49 -0
- package/CONTRIBUTING.md +161 -0
- package/ISSUES.md +601 -0
- package/Makefile +70 -0
- package/README.md +80 -0
- package/automation/ci-issue-reporter.sh +237 -0
- package/config.example.json +18 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +111 -0
- package/dist/shell.d.ts +50 -0
- package/dist/shell.js +209 -0
- package/dist/tools/apps.d.ts +3 -0
- package/dist/tools/apps.js +63 -0
- package/dist/tools/audio.d.ts +3 -0
- package/dist/tools/audio.js +142 -0
- package/dist/tools/audio_apps.d.ts +3 -0
- package/dist/tools/audio_apps.js +86 -0
- package/dist/tools/automation.d.ts +3 -0
- package/dist/tools/automation.js +261 -0
- package/dist/tools/bluetooth.d.ts +3 -0
- package/dist/tools/bluetooth.js +96 -0
- package/dist/tools/clipboard.d.ts +3 -0
- package/dist/tools/clipboard.js +118 -0
- package/dist/tools/config.d.ts +3 -0
- package/dist/tools/config.js +85 -0
- package/dist/tools/dialog.d.ts +3 -0
- package/dist/tools/dialog.js +72 -0
- package/dist/tools/display.d.ts +3 -0
- package/dist/tools/display.js +256 -0
- package/dist/tools/drives.d.ts +3 -0
- package/dist/tools/drives.js +98 -0
- package/dist/tools/environment.d.ts +3 -0
- package/dist/tools/environment.js +129 -0
- package/dist/tools/execute.d.ts +3 -0
- package/dist/tools/execute.js +28 -0
- package/dist/tools/filesystem.d.ts +3 -0
- package/dist/tools/filesystem.js +230 -0
- package/dist/tools/firewall.d.ts +3 -0
- package/dist/tools/firewall.js +108 -0
- package/dist/tools/hosts.d.ts +3 -0
- package/dist/tools/hosts.js +119 -0
- package/dist/tools/maintenance.d.ts +3 -0
- package/dist/tools/maintenance.js +236 -0
- package/dist/tools/netstat.d.ts +3 -0
- package/dist/tools/netstat.js +56 -0
- package/dist/tools/network.d.ts +3 -0
- package/dist/tools/network.js +70 -0
- package/dist/tools/notification.d.ts +3 -0
- package/dist/tools/notification.js +41 -0
- package/dist/tools/power.d.ts +3 -0
- package/dist/tools/power.js +104 -0
- package/dist/tools/printer.d.ts +3 -0
- package/dist/tools/printer.js +97 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +54 -0
- package/dist/tools/process_kill.d.ts +3 -0
- package/dist/tools/process_kill.js +48 -0
- package/dist/tools/recycle_bin.d.ts +3 -0
- package/dist/tools/recycle_bin.js +108 -0
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/registry.js +136 -0
- package/dist/tools/scheduler.d.ts +3 -0
- package/dist/tools/scheduler.js +116 -0
- package/dist/tools/service.d.ts +3 -0
- package/dist/tools/service.js +79 -0
- package/dist/tools/startup.d.ts +3 -0
- package/dist/tools/startup.js +159 -0
- package/dist/tools/storage.d.ts +3 -0
- package/dist/tools/storage.js +129 -0
- package/dist/tools/system.d.ts +3 -0
- package/dist/tools/system.js +84 -0
- package/dist/tools/system_mgmt.d.ts +3 -0
- package/dist/tools/system_mgmt.js +174 -0
- package/dist/tools/terminal.d.ts +3 -0
- package/dist/tools/terminal.js +80 -0
- package/dist/tools/theme.d.ts +3 -0
- package/dist/tools/theme.js +165 -0
- package/dist/tools/usb.d.ts +3 -0
- package/dist/tools/usb.js +52 -0
- package/dist/tools/virtual_desktop.d.ts +3 -0
- package/dist/tools/virtual_desktop.js +112 -0
- package/dist/tools/wifi.d.ts +3 -0
- package/dist/tools/wifi.js +136 -0
- package/dist/tools/window.d.ts +3 -0
- package/dist/tools/window.js +189 -0
- package/dist/tools/winget.d.ts +3 -0
- package/dist/tools/winget.js +79 -0
- package/dist/tools/wsl.d.ts +3 -0
- package/dist/tools/wsl.js +99 -0
- package/docs/API.md +63 -0
- package/docs/ARCHITECTURE.md +73 -0
- package/docs/INSTALLATION.md +102 -0
- package/docs/index.md +12 -0
- package/package.json +35 -0
- package/scripts/setup.mjs +123 -0
- package/src/index.ts +125 -0
- package/src/shell.ts +253 -0
- package/src/tools/apps.ts +76 -0
- package/src/tools/audio.ts +161 -0
- package/src/tools/audio_apps.ts +98 -0
- package/src/tools/automation.ts +297 -0
- package/src/tools/bluetooth.ts +114 -0
- package/src/tools/clipboard.ts +138 -0
- package/src/tools/config.ts +105 -0
- package/src/tools/dialog.ts +87 -0
- package/src/tools/display.ts +285 -0
- package/src/tools/drives.ts +124 -0
- package/src/tools/environment.ts +146 -0
- package/src/tools/execute.ts +35 -0
- package/src/tools/filesystem.ts +273 -0
- package/src/tools/firewall.ts +125 -0
- package/src/tools/hosts.ts +135 -0
- package/src/tools/maintenance.ts +299 -0
- package/src/tools/netstat.ts +72 -0
- package/src/tools/network.ts +84 -0
- package/src/tools/notification.ts +50 -0
- package/src/tools/power.ts +123 -0
- package/src/tools/printer.ts +114 -0
- package/src/tools/process.ts +80 -0
- package/src/tools/process_kill.ts +57 -0
- package/src/tools/recycle_bin.ts +126 -0
- package/src/tools/registry.ts +165 -0
- package/src/tools/scheduler.ts +140 -0
- package/src/tools/service.ts +102 -0
- package/src/tools/startup.ts +180 -0
- package/src/tools/storage.ts +141 -0
- package/src/tools/system.ts +99 -0
- package/src/tools/system_mgmt.ts +190 -0
- package/src/tools/terminal.ts +117 -0
- package/src/tools/theme.ts +205 -0
- package/src/tools/usb.ts +65 -0
- package/src/tools/virtual_desktop.ts +122 -0
- package/src/tools/wifi.ts +157 -0
- package/src/tools/window.ts +211 -0
- package/src/tools/winget.ts +100 -0
- package/src/tools/wsl.ts +112 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_startup_list (#33), windows_startup_manage (#34)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerStartupTools(server) {
|
|
9
|
+
server.tool('windows_startup_list', 'List all startup items from registry (Run/RunOnce), startup folder, and scheduled logon tasks.', {}, async () => {
|
|
10
|
+
const ps = `
|
|
11
|
+
$items = [System.Collections.Generic.List[PSObject]]::new()
|
|
12
|
+
|
|
13
|
+
# Registry: HKCU Run
|
|
14
|
+
$regPaths = @(
|
|
15
|
+
@{ Path = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'; Scope = 'User'; Source = 'Registry Run' }
|
|
16
|
+
@{ Path = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce'; Scope = 'User'; Source = 'Registry RunOnce' }
|
|
17
|
+
@{ Path = 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'; Scope = 'System'; Source = 'Registry Run' }
|
|
18
|
+
@{ Path = 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce'; Scope = 'System'; Source = 'Registry RunOnce' }
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
foreach ($rp in $regPaths) {
|
|
22
|
+
if (Test-Path $rp.Path) {
|
|
23
|
+
$props = Get-ItemProperty -Path $rp.Path -ErrorAction SilentlyContinue
|
|
24
|
+
if ($props) {
|
|
25
|
+
$props.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object {
|
|
26
|
+
$items.Add([PSCustomObject]@{
|
|
27
|
+
Name = $_.Name
|
|
28
|
+
Command = $_.Value
|
|
29
|
+
Source = $rp.Source
|
|
30
|
+
Scope = $rp.Scope
|
|
31
|
+
Enabled = $true
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Startup folder
|
|
39
|
+
$userStartup = [Environment]::GetFolderPath('Startup')
|
|
40
|
+
$commonStartup = [Environment]::GetFolderPath('CommonStartup')
|
|
41
|
+
foreach ($folder in @(@{Path=$userStartup;Scope='User'}, @{Path=$commonStartup;Scope='System'})) {
|
|
42
|
+
if (Test-Path $folder.Path) {
|
|
43
|
+
Get-ChildItem -Path $folder.Path -File -ErrorAction SilentlyContinue | ForEach-Object {
|
|
44
|
+
$items.Add([PSCustomObject]@{
|
|
45
|
+
Name = $_.BaseName
|
|
46
|
+
Command = $_.FullName
|
|
47
|
+
Source = 'Startup Folder'
|
|
48
|
+
Scope = $folder.Scope
|
|
49
|
+
Enabled = $true
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Scheduled tasks at logon
|
|
56
|
+
Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object {
|
|
57
|
+
$_.Triggers | Where-Object { $_ -is [CimInstance] -and $_.CimClass.CimClassName -eq 'MSFT_TaskLogonTrigger' }
|
|
58
|
+
} | ForEach-Object {
|
|
59
|
+
$items.Add([PSCustomObject]@{
|
|
60
|
+
Name = $_.TaskName
|
|
61
|
+
Command = ($_.Actions | ForEach-Object { $_.Execute }) -join ' '
|
|
62
|
+
Source = 'Task Scheduler (Logon)'
|
|
63
|
+
Scope = 'System'
|
|
64
|
+
Enabled = $_.State -eq 'Ready'
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
$items | ConvertTo-Json -Depth 3 -Compress`;
|
|
69
|
+
const result = await runPowerShell(ps, { timeout: 20000 });
|
|
70
|
+
if (result.exitCode !== 0) {
|
|
71
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
72
|
+
}
|
|
73
|
+
if (!result.stdout) {
|
|
74
|
+
return { content: [{ type: 'text', text: 'No startup items found.' }] };
|
|
75
|
+
}
|
|
76
|
+
const items = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
77
|
+
const lines = items.map((i) => {
|
|
78
|
+
const status = i.Enabled ? '[ON] ' : '[OFF]';
|
|
79
|
+
return `${status} ${i.Scope.padEnd(6)} ${i.Source.padEnd(24)} ${i.Name.padEnd(30).slice(0, 30)} ${(i.Command || '').slice(0, 50)}`;
|
|
80
|
+
});
|
|
81
|
+
const header = `State ${'Scope'.padEnd(6)} ${'Source'.padEnd(24)} ${'Name'.padEnd(30)} Command`;
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text: `${header}\n${'─'.repeat(110)}\n${lines.join('\n')}\n\n${items.length} startup items` }],
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
server.tool('windows_startup_manage', 'Add, remove, enable, or disable a startup item.', {
|
|
87
|
+
action: z.enum(['add', 'remove', 'enable', 'disable']).describe('Action'),
|
|
88
|
+
name: z.string().describe('Startup item name'),
|
|
89
|
+
command: z.string().optional().describe('Command to run at startup (for add)'),
|
|
90
|
+
location: z.enum(['registry', 'startup_folder']).default('registry').describe('Where to add (for add)'),
|
|
91
|
+
}, async ({ action, name, command, location }) => {
|
|
92
|
+
let ps;
|
|
93
|
+
switch (action) {
|
|
94
|
+
case 'add':
|
|
95
|
+
if (!command) {
|
|
96
|
+
return { content: [{ type: 'text', text: 'Add requires a command.' }], isError: true };
|
|
97
|
+
}
|
|
98
|
+
if (location === 'startup_folder') {
|
|
99
|
+
ps = `
|
|
100
|
+
$startupPath = [Environment]::GetFolderPath('Startup')
|
|
101
|
+
$shortcutPath = Join-Path $startupPath '${name.replace(/'/g, "''")}.lnk'
|
|
102
|
+
$ws = New-Object -ComObject WScript.Shell
|
|
103
|
+
$sc = $ws.CreateShortcut($shortcutPath)
|
|
104
|
+
$sc.TargetPath = '${command.replace(/'/g, "''")}'
|
|
105
|
+
$sc.Save()
|
|
106
|
+
"Added startup shortcut: $shortcutPath"`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
ps = `
|
|
110
|
+
Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${name.replace(/'/g, "''")}' -Value '${command.replace(/'/g, "''")}' -ErrorAction Stop
|
|
111
|
+
"Added to HKCU Run: ${name}"`;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
case 'remove':
|
|
115
|
+
ps = `
|
|
116
|
+
$removed = $false
|
|
117
|
+
# Try registry
|
|
118
|
+
$regPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
|
|
119
|
+
if ((Get-ItemProperty -Path $regPath -Name '${name.replace(/'/g, "''")}' -ErrorAction SilentlyContinue)) {
|
|
120
|
+
Remove-ItemProperty -Path $regPath -Name '${name.replace(/'/g, "''")}' -ErrorAction Stop
|
|
121
|
+
$removed = $true
|
|
122
|
+
}
|
|
123
|
+
# Try startup folder
|
|
124
|
+
$startupPath = [Environment]::GetFolderPath('Startup')
|
|
125
|
+
$lnk = Join-Path $startupPath '${name.replace(/'/g, "''")}.lnk'
|
|
126
|
+
if (Test-Path $lnk) { Remove-Item $lnk -Force; $removed = $true }
|
|
127
|
+
if ($removed) { "Removed: ${name}" } else { "Not found: ${name}" }`;
|
|
128
|
+
break;
|
|
129
|
+
case 'disable':
|
|
130
|
+
ps = `
|
|
131
|
+
$regPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
|
|
132
|
+
$val = (Get-ItemProperty -Path $regPath -Name '${name.replace(/'/g, "''")}' -ErrorAction SilentlyContinue).'${name.replace(/'/g, "''")}'
|
|
133
|
+
if ($val) {
|
|
134
|
+
Remove-ItemProperty -Path $regPath -Name '${name.replace(/'/g, "''")}' -ErrorAction Stop
|
|
135
|
+
$disabledPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run_Disabled'
|
|
136
|
+
if (-not (Test-Path $disabledPath)) { New-Item -Path $disabledPath -Force | Out-Null }
|
|
137
|
+
Set-ItemProperty -Path $disabledPath -Name '${name.replace(/'/g, "''")}' -Value $val
|
|
138
|
+
"Disabled: ${name}"
|
|
139
|
+
} else { "Not found in registry Run: ${name}" }`;
|
|
140
|
+
break;
|
|
141
|
+
case 'enable':
|
|
142
|
+
ps = `
|
|
143
|
+
$disabledPath = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run_Disabled'
|
|
144
|
+
$val = (Get-ItemProperty -Path $disabledPath -Name '${name.replace(/'/g, "''")}' -ErrorAction SilentlyContinue).'${name.replace(/'/g, "''")}'
|
|
145
|
+
if ($val) {
|
|
146
|
+
Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${name.replace(/'/g, "''")}' -Value $val
|
|
147
|
+
Remove-ItemProperty -Path $disabledPath -Name '${name.replace(/'/g, "''")}' -ErrorAction SilentlyContinue
|
|
148
|
+
"Enabled: ${name}"
|
|
149
|
+
} else { "Not found in disabled items: ${name}" }`;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: 'text', text: result.stdout || result.stderr }],
|
|
155
|
+
isError: result.exitCode !== 0,
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=startup.js.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_disk_cleanup (#72), windows_symlink (#73)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { runPowerShell } from '../shell.js';
|
|
8
|
+
export function registerStorageTools(server) {
|
|
9
|
+
server.tool('windows_disk_cleanup', 'Analyze disk usage and clean temp files, caches, and logs.', {
|
|
10
|
+
action: z.enum(['analyze', 'clean']).default('analyze').describe('Analyze or clean'),
|
|
11
|
+
drive: z.string().default('C:').describe('Drive letter'),
|
|
12
|
+
}, async ({ action, drive }) => {
|
|
13
|
+
if (action === 'analyze') {
|
|
14
|
+
const ps = `
|
|
15
|
+
$tempUser = [IO.Path]::GetTempPath()
|
|
16
|
+
$tempWin = "$env:WINDIR\\Temp"
|
|
17
|
+
$downloads = [Environment]::GetFolderPath('UserProfile') + '\\Downloads'
|
|
18
|
+
|
|
19
|
+
function Get-FolderSize($path) {
|
|
20
|
+
if (Test-Path $path) {
|
|
21
|
+
$size = (Get-ChildItem $path -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
|
|
22
|
+
[math]::Round($size / 1MB, 1)
|
|
23
|
+
} else { 0 }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='${drive.replace(/'/g, "''")}'"
|
|
27
|
+
|
|
28
|
+
[PSCustomObject]@{
|
|
29
|
+
Drive = '${drive}'
|
|
30
|
+
TotalGB = [math]::Round($disk.Size / 1GB, 1)
|
|
31
|
+
FreeGB = [math]::Round($disk.FreeSpace / 1GB, 1)
|
|
32
|
+
UsedPct = [math]::Round(($disk.Size - $disk.FreeSpace) / $disk.Size * 100, 1)
|
|
33
|
+
TempUserMB = Get-FolderSize $tempUser
|
|
34
|
+
TempWindowsMB = Get-FolderSize $tempWin
|
|
35
|
+
DownloadsMB = Get-FolderSize $downloads
|
|
36
|
+
RecycleBinItems = (New-Object -ComObject Shell.Application).Namespace(10).Items().Count
|
|
37
|
+
} | ConvertTo-Json -Compress`;
|
|
38
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
39
|
+
if (result.exitCode !== 0) {
|
|
40
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
41
|
+
}
|
|
42
|
+
const d = JSON.parse(result.stdout);
|
|
43
|
+
return {
|
|
44
|
+
content: [{
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: [
|
|
47
|
+
`${d.Drive} — ${d.FreeGB}/${d.TotalGB} GB free (${d.UsedPct}% used)`,
|
|
48
|
+
``,
|
|
49
|
+
`Cleanable:`,
|
|
50
|
+
` User temp: ${d.TempUserMB} MB`,
|
|
51
|
+
` Windows temp: ${d.TempWindowsMB} MB`,
|
|
52
|
+
` Downloads: ${d.DownloadsMB} MB`,
|
|
53
|
+
` Recycle Bin: ${d.RecycleBinItems} items`,
|
|
54
|
+
].join('\n'),
|
|
55
|
+
}],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const ps = `
|
|
59
|
+
$before = (Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='${drive.replace(/'/g, "''")}'").FreeSpace
|
|
60
|
+
Remove-Item "$env:TEMP\\*" -Recurse -Force -ErrorAction SilentlyContinue
|
|
61
|
+
Remove-Item "$env:WINDIR\\Temp\\*" -Recurse -Force -ErrorAction SilentlyContinue
|
|
62
|
+
$after = (Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='${drive.replace(/'/g, "''")}'").FreeSpace
|
|
63
|
+
$freed = [math]::Round(($after - $before) / 1MB, 1)
|
|
64
|
+
"Cleaned temp files. Freed: $freed MB"`;
|
|
65
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
66
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
67
|
+
});
|
|
68
|
+
server.tool('windows_symlink', 'Create and manage symbolic links, hard links, and directory junctions.', {
|
|
69
|
+
action: z.enum(['create', 'list', 'resolve', 'remove']).default('list').describe('Action'),
|
|
70
|
+
target: z.string().optional().describe('Target path (what the link points to)'),
|
|
71
|
+
link: z.string().optional().describe('Link path (the symlink itself)'),
|
|
72
|
+
type: z.enum(['symlink', 'junction', 'hardlink']).default('symlink').describe('Link type (for create)'),
|
|
73
|
+
path: z.string().optional().describe('Directory to list symlinks in (for list)'),
|
|
74
|
+
}, async ({ action, target, link, type, path }) => {
|
|
75
|
+
switch (action) {
|
|
76
|
+
case 'create': {
|
|
77
|
+
if (!target || !link) {
|
|
78
|
+
return { content: [{ type: 'text', text: 'create requires target and link.' }], isError: true };
|
|
79
|
+
}
|
|
80
|
+
let cmd;
|
|
81
|
+
if (type === 'junction') {
|
|
82
|
+
cmd = `cmd /c mklink /J "${link}" "${target}"`;
|
|
83
|
+
}
|
|
84
|
+
else if (type === 'hardlink') {
|
|
85
|
+
cmd = `cmd /c mklink /H "${link}" "${target}"`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
cmd = `cmd /c mklink ${target.includes('.') ? '' : '/D'} "${link}" "${target}"`;
|
|
89
|
+
}
|
|
90
|
+
const ps = `& { ${cmd} } 2>&1`;
|
|
91
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
92
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
93
|
+
}
|
|
94
|
+
case 'list': {
|
|
95
|
+
const dir = path || '.';
|
|
96
|
+
const ps = `
|
|
97
|
+
Get-ChildItem -Path '${dir.replace(/'/g, "''")}' -Force -ErrorAction Stop | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint } | ForEach-Object {
|
|
98
|
+
[PSCustomObject]@{
|
|
99
|
+
Name = $_.Name
|
|
100
|
+
Target = $_.Target
|
|
101
|
+
Type = if ($_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
102
|
+
LinkType = $_.LinkType
|
|
103
|
+
}
|
|
104
|
+
} | ConvertTo-Json -Depth 3 -Compress`;
|
|
105
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
106
|
+
if (!result.stdout)
|
|
107
|
+
return { content: [{ type: 'text', text: 'No symlinks found.' }] };
|
|
108
|
+
const links = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
109
|
+
const lines = links.map((l) => ` ${(l.LinkType || 'Link').padEnd(12)} ${l.Name.padEnd(30)} -> ${l.Target}`);
|
|
110
|
+
return { content: [{ type: 'text', text: `Symlinks in ${dir}:\n${lines.join('\n')}` }] };
|
|
111
|
+
}
|
|
112
|
+
case 'resolve': {
|
|
113
|
+
if (!link)
|
|
114
|
+
return { content: [{ type: 'text', text: 'resolve requires link path.' }], isError: true };
|
|
115
|
+
const ps = `(Get-Item '${link.replace(/'/g, "''")}' -Force).Target`;
|
|
116
|
+
const result = await runPowerShell(ps, { timeout: 5000 });
|
|
117
|
+
return { content: [{ type: 'text', text: `${link} -> ${result.stdout || '(not a symlink)'}` }] };
|
|
118
|
+
}
|
|
119
|
+
case 'remove': {
|
|
120
|
+
if (!link)
|
|
121
|
+
return { content: [{ type: 'text', text: 'remove requires link path.' }], isError: true };
|
|
122
|
+
const ps = `Remove-Item '${link.replace(/'/g, "''")}' -Force -ErrorAction Stop; "Removed: ${link}"`;
|
|
123
|
+
const result = await runPowerShell(ps, { timeout: 5000 });
|
|
124
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_system_info (#18)
|
|
5
|
+
*/
|
|
6
|
+
import { runPowerShell } from '../shell.js';
|
|
7
|
+
export function registerSystemTools(server) {
|
|
8
|
+
server.tool('windows_system_info', 'Get comprehensive system information: OS, CPU, RAM, disk, network, uptime.', {}, async () => {
|
|
9
|
+
const ps = `
|
|
10
|
+
$os = Get-CimInstance Win32_OperatingSystem
|
|
11
|
+
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
|
12
|
+
$cs = Get-CimInstance Win32_ComputerSystem
|
|
13
|
+
$disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
|
|
14
|
+
[PSCustomObject]@{
|
|
15
|
+
Drive = $_.DeviceID
|
|
16
|
+
Label = $_.VolumeName
|
|
17
|
+
FileSystem = $_.FileSystem
|
|
18
|
+
TotalGB = [math]::Round($_.Size / 1GB, 1)
|
|
19
|
+
FreeGB = [math]::Round($_.FreeSpace / 1GB, 1)
|
|
20
|
+
UsedPct = if ($_.Size -gt 0) { [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1) } else { 0 }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
$adapters = Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Up' } | ForEach-Object {
|
|
24
|
+
$ip = (Get-NetIPAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue).IPAddress
|
|
25
|
+
[PSCustomObject]@{
|
|
26
|
+
Name = $_.Name
|
|
27
|
+
Speed = $_.LinkSpeed
|
|
28
|
+
IP = $ip
|
|
29
|
+
MAC = $_.MacAddress
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
$uptime = (Get-Date) - $os.LastBootUpTime
|
|
33
|
+
|
|
34
|
+
[PSCustomObject]@{
|
|
35
|
+
OS = "$($os.Caption) $($os.Version) Build $($os.BuildNumber)"
|
|
36
|
+
Edition = $os.OperatingSystemSKU
|
|
37
|
+
Architecture = $os.OSArchitecture
|
|
38
|
+
Hostname = $env:COMPUTERNAME
|
|
39
|
+
Username = $env:USERNAME
|
|
40
|
+
Domain = $cs.Domain
|
|
41
|
+
CPU = "$($cpu.Name)"
|
|
42
|
+
CPUCores = "$($cpu.NumberOfCores) cores / $($cpu.NumberOfLogicalProcessors) threads"
|
|
43
|
+
CPUUsage = "$([math]::Round($cpu.LoadPercentage, 0))%"
|
|
44
|
+
RAMTotalGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
|
|
45
|
+
RAMAvailGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
|
|
46
|
+
RAMUsedPct = [math]::Round(($cs.TotalPhysicalMemory - $os.FreePhysicalMemory * 1KB) / $cs.TotalPhysicalMemory * 100, 1)
|
|
47
|
+
Disks = $disks
|
|
48
|
+
Network = $adapters
|
|
49
|
+
Uptime = "$($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m"
|
|
50
|
+
} | ConvertTo-Json -Depth 4 -Compress`;
|
|
51
|
+
const result = await runPowerShell(ps, { timeout: 45000 });
|
|
52
|
+
if (result.exitCode !== 0) {
|
|
53
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
54
|
+
}
|
|
55
|
+
const info = JSON.parse(result.stdout);
|
|
56
|
+
const diskLines = (Array.isArray(info.Disks) ? info.Disks : [info.Disks])
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map((d) => ` ${d.Drive} ${d.Label || ''} — ${d.FreeGB}/${d.TotalGB} GB free (${d.UsedPct}% used)`);
|
|
59
|
+
const netLines = (Array.isArray(info.Network) ? info.Network : [info.Network])
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map((n) => ` ${n.Name} — ${n.IP || 'no IP'} (${n.Speed})`);
|
|
62
|
+
const text = [
|
|
63
|
+
`OS: ${info.OS}`,
|
|
64
|
+
`Architecture: ${info.Architecture}`,
|
|
65
|
+
`Host: ${info.Hostname} (${info.Domain})`,
|
|
66
|
+
`User: ${info.Username}`,
|
|
67
|
+
`Uptime: ${info.Uptime}`,
|
|
68
|
+
``,
|
|
69
|
+
`CPU: ${info.CPU}`,
|
|
70
|
+
`Cores: ${info.CPUCores}`,
|
|
71
|
+
`CPU Usage: ${info.CPUUsage}`,
|
|
72
|
+
``,
|
|
73
|
+
`RAM: ${info.RAMAvailGB}/${info.RAMTotalGB} GB available (${info.RAMUsedPct}% used)`,
|
|
74
|
+
``,
|
|
75
|
+
`Disks:`,
|
|
76
|
+
...diskLines,
|
|
77
|
+
``,
|
|
78
|
+
`Network:`,
|
|
79
|
+
...netLines,
|
|
80
|
+
].join('\n');
|
|
81
|
+
return { content: [{ type: 'text', text }] };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=system.js.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tools: windows_timezone (#74), windows_features (#75),
|
|
5
|
+
* windows_smb_shares (#76), windows_dns_cache (#77)
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { runPowerShell } from '../shell.js';
|
|
9
|
+
export function registerSystemMgmtTools(server) {
|
|
10
|
+
server.tool('windows_timezone', 'Get or set system timezone. List available timezones or sync time.', {
|
|
11
|
+
action: z.enum(['get', 'list', 'set', 'sync']).default('get').describe('Action'),
|
|
12
|
+
timezone: z.string().optional().describe('Timezone ID (for set, e.g. "Eastern Standard Time")'),
|
|
13
|
+
filter: z.string().optional().describe('Filter timezone list'),
|
|
14
|
+
}, async ({ action, timezone, filter }) => {
|
|
15
|
+
switch (action) {
|
|
16
|
+
case 'get': {
|
|
17
|
+
const ps = `
|
|
18
|
+
$tz = Get-TimeZone
|
|
19
|
+
[PSCustomObject]@{
|
|
20
|
+
Id = $tz.Id
|
|
21
|
+
DisplayName = $tz.DisplayName
|
|
22
|
+
UTCOffset = $tz.BaseUtcOffset.ToString()
|
|
23
|
+
DST = $tz.SupportsDaylightSavingTime
|
|
24
|
+
DSTActive = (Get-Date).IsDaylightSavingTime()
|
|
25
|
+
CurrentTime = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
|
|
26
|
+
UTCTime = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')
|
|
27
|
+
} | ConvertTo-Json -Compress`;
|
|
28
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
29
|
+
if (result.exitCode !== 0)
|
|
30
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
31
|
+
const tz = JSON.parse(result.stdout);
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: `Timezone: ${tz.Id}\n${tz.DisplayName}\nUTC Offset: ${tz.UTCOffset}${tz.DSTActive ? ' (DST active)' : ''}\nLocal: ${tz.CurrentTime}\nUTC: ${tz.UTCTime}`,
|
|
36
|
+
}],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
case 'list': {
|
|
40
|
+
const filterClause = filter ? `| Where-Object { $_.Id -like '*${filter.replace(/'/g, "''")}*' -or $_.DisplayName -like '*${filter.replace(/'/g, "''")}*' }` : '';
|
|
41
|
+
const ps = `Get-TimeZone -ListAvailable ${filterClause} | ForEach-Object { "$($_.BaseUtcOffset.ToString().PadRight(9)) $($_.Id)" }`;
|
|
42
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
43
|
+
return { content: [{ type: 'text', text: result.stdout || 'No timezones found.' }] };
|
|
44
|
+
}
|
|
45
|
+
case 'set': {
|
|
46
|
+
if (!timezone)
|
|
47
|
+
return { content: [{ type: 'text', text: 'set requires timezone.' }], isError: true };
|
|
48
|
+
const ps = `Set-TimeZone -Id '${timezone.replace(/'/g, "''")}' -ErrorAction Stop; "Timezone set to: ${timezone}"`;
|
|
49
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
50
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }], isError: result.exitCode !== 0 };
|
|
51
|
+
}
|
|
52
|
+
case 'sync': {
|
|
53
|
+
const ps = `w32tm /resync /force 2>&1; "Time sync requested"`;
|
|
54
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
55
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
server.tool('windows_features', 'List, enable, or disable Windows optional features (WSL, Hyper-V, Sandbox, etc.).', {
|
|
60
|
+
action: z.enum(['list', 'enable', 'disable']).default('list').describe('Action'),
|
|
61
|
+
name: z.string().optional().describe('Feature name (for enable/disable)'),
|
|
62
|
+
filter: z.string().optional().describe('Filter by name (for list)'),
|
|
63
|
+
}, async ({ action, name, filter }) => {
|
|
64
|
+
if (action === 'list') {
|
|
65
|
+
const filterClause = filter ? `| Where-Object { $_.FeatureName -like '*${filter.replace(/'/g, "''")}*' }` : '';
|
|
66
|
+
const ps = `
|
|
67
|
+
try {
|
|
68
|
+
Get-WindowsOptionalFeature -Online -ErrorAction Stop ${filterClause} | Select-Object FeatureName,State | Sort-Object FeatureName | ConvertTo-Json -Depth 3 -Compress
|
|
69
|
+
} catch {
|
|
70
|
+
if ($_.Exception.Message -match 'elevation') { Write-Output 'NEEDS_ELEVATION' }
|
|
71
|
+
else { throw }
|
|
72
|
+
}`;
|
|
73
|
+
const result = await runPowerShell(ps, { timeout: 30000 });
|
|
74
|
+
if (result.stdout?.trim() === 'NEEDS_ELEVATION') {
|
|
75
|
+
return { content: [{ type: 'text', text: 'Windows Optional Features requires elevation (run as Administrator).' }] };
|
|
76
|
+
}
|
|
77
|
+
if (result.exitCode !== 0)
|
|
78
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
79
|
+
if (!result.stdout)
|
|
80
|
+
return { content: [{ type: 'text', text: 'No features found.' }] };
|
|
81
|
+
const features = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
82
|
+
const lines = features.map((f) => {
|
|
83
|
+
const state = f.State === 2 ? '[ON] ' : '[OFF]';
|
|
84
|
+
return `${state} ${f.FeatureName}`;
|
|
85
|
+
});
|
|
86
|
+
return { content: [{ type: 'text', text: `${lines.join('\n')}\n\n${features.length} features` }] };
|
|
87
|
+
}
|
|
88
|
+
if (!name)
|
|
89
|
+
return { content: [{ type: 'text', text: `${action} requires name.` }], isError: true };
|
|
90
|
+
const cmd = action === 'enable'
|
|
91
|
+
? `Enable-WindowsOptionalFeature -Online -FeatureName '${name.replace(/'/g, "''")}' -NoRestart -ErrorAction Stop`
|
|
92
|
+
: `Disable-WindowsOptionalFeature -Online -FeatureName '${name.replace(/'/g, "''")}' -NoRestart -ErrorAction Stop`;
|
|
93
|
+
const ps = `${cmd} | Select-Object RestartNeeded | ConvertTo-Json -Compress`;
|
|
94
|
+
const result = await runPowerShell(ps, { timeout: 60000 });
|
|
95
|
+
if (result.exitCode !== 0)
|
|
96
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
97
|
+
const info = JSON.parse(result.stdout);
|
|
98
|
+
return { content: [{ type: 'text', text: `${name}: ${action}d${info.RestartNeeded ? ' (restart required)' : ''}` }] };
|
|
99
|
+
});
|
|
100
|
+
server.tool('windows_smb_shares', 'List local shares, mapped drives, or map/unmap network drives.', {
|
|
101
|
+
action: z.enum(['list_shares', 'list_mapped', 'map', 'unmap']).default('list_shares').describe('Action'),
|
|
102
|
+
letter: z.string().optional().describe('Drive letter (for map/unmap, e.g. "Z:")'),
|
|
103
|
+
unc_path: z.string().optional().describe('UNC path (for map, e.g. "\\\\\\\\server\\\\share")'),
|
|
104
|
+
}, async ({ action, letter, unc_path }) => {
|
|
105
|
+
switch (action) {
|
|
106
|
+
case 'list_shares': {
|
|
107
|
+
const ps = `Get-SmbShare -ErrorAction Stop | Select-Object Name,Path,Description | ConvertTo-Json -Depth 3 -Compress`;
|
|
108
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
109
|
+
if (result.exitCode !== 0)
|
|
110
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
111
|
+
if (!result.stdout)
|
|
112
|
+
return { content: [{ type: 'text', text: 'No shares.' }] };
|
|
113
|
+
const shares = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
114
|
+
const lines = shares.map((s) => ` ${s.Name.padEnd(20)} ${(s.Path || '').padEnd(30)} ${s.Description || ''}`);
|
|
115
|
+
return { content: [{ type: 'text', text: `Local shares:\n${lines.join('\n')}` }] };
|
|
116
|
+
}
|
|
117
|
+
case 'list_mapped': {
|
|
118
|
+
const ps = `Get-PSDrive -PSProvider FileSystem | Where-Object { $_.DisplayRoot } | ForEach-Object { "$($_.Name): $($_.DisplayRoot)" }`;
|
|
119
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
120
|
+
return { content: [{ type: 'text', text: result.stdout || 'No mapped drives.' }] };
|
|
121
|
+
}
|
|
122
|
+
case 'map': {
|
|
123
|
+
if (!letter || !unc_path)
|
|
124
|
+
return { content: [{ type: 'text', text: 'map requires letter and unc_path.' }], isError: true };
|
|
125
|
+
const ps = `net use ${letter} "${unc_path}" /persistent:yes 2>&1`;
|
|
126
|
+
const result = await runPowerShell(ps, { timeout: 15000 });
|
|
127
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
128
|
+
}
|
|
129
|
+
case 'unmap': {
|
|
130
|
+
if (!letter)
|
|
131
|
+
return { content: [{ type: 'text', text: 'unmap requires letter.' }], isError: true };
|
|
132
|
+
const ps = `net use ${letter} /delete /yes 2>&1`;
|
|
133
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
134
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
server.tool('windows_dns_cache', 'View, filter, or clear the DNS resolver cache. Also resolves domain names.', {
|
|
139
|
+
action: z.enum(['list', 'clear', 'resolve']).default('list').describe('Action'),
|
|
140
|
+
filter: z.string().optional().describe('Filter by domain (for list)'),
|
|
141
|
+
domain: z.string().optional().describe('Domain to resolve (for resolve)'),
|
|
142
|
+
limit: z.number().default(30).describe('Max entries (for list)'),
|
|
143
|
+
}, async ({ action, filter, domain, limit }) => {
|
|
144
|
+
switch (action) {
|
|
145
|
+
case 'list': {
|
|
146
|
+
const filterClause = filter ? `| Where-Object { $_.Entry -like '*${filter.replace(/'/g, "''")}*' }` : '';
|
|
147
|
+
const ps = `Get-DnsClientCache ${filterClause} -ErrorAction SilentlyContinue | Select-Object -First ${limit} Entry,RecordName,RecordType,Data,TimeToLive | ConvertTo-Json -Depth 3 -Compress`;
|
|
148
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
149
|
+
if (!result.stdout)
|
|
150
|
+
return { content: [{ type: 'text', text: 'DNS cache is empty.' }] };
|
|
151
|
+
const entries = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
152
|
+
const lines = entries.map((e) => ` ${(e.Entry || '').padEnd(40).slice(0, 40)} ${String(e.RecordType).padEnd(5)} ${String(e.TimeToLive).padStart(6)}s ${e.Data || ''}`);
|
|
153
|
+
return { content: [{ type: 'text', text: `DNS Cache (${entries.length} entries):\n${lines.join('\n')}` }] };
|
|
154
|
+
}
|
|
155
|
+
case 'clear': {
|
|
156
|
+
const ps = `Clear-DnsClientCache; "DNS cache cleared"`;
|
|
157
|
+
const result = await runPowerShell(ps, { timeout: 5000 });
|
|
158
|
+
return { content: [{ type: 'text', text: result.stdout || result.stderr }] };
|
|
159
|
+
}
|
|
160
|
+
case 'resolve': {
|
|
161
|
+
if (!domain)
|
|
162
|
+
return { content: [{ type: 'text', text: 'resolve requires domain.' }], isError: true };
|
|
163
|
+
const ps = `Resolve-DnsName '${domain.replace(/'/g, "''")}' -ErrorAction Stop | Select-Object Name,Type,IPAddress,NameHost | ConvertTo-Json -Depth 3 -Compress`;
|
|
164
|
+
const result = await runPowerShell(ps, { timeout: 10000 });
|
|
165
|
+
if (result.exitCode !== 0)
|
|
166
|
+
return { content: [{ type: 'text', text: `Error: ${result.stderr}` }], isError: true };
|
|
167
|
+
const records = Array.isArray(JSON.parse(result.stdout)) ? JSON.parse(result.stdout) : [JSON.parse(result.stdout)];
|
|
168
|
+
const lines = records.map((r) => ` ${r.Name} ${r.IPAddress || r.NameHost || ''}`);
|
|
169
|
+
return { content: [{ type: 'text', text: `${domain}:\n${lines.join('\n')}` }] };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=system_mgmt.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
*
|
|
4
|
+
* Tool: windows_terminal_session — Persistent interactive terminals (#35)
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { startSession, sendToSession, readSessionOutput, listSessions, terminateSession, } from '../shell.js';
|
|
8
|
+
export function registerTerminalTools(server) {
|
|
9
|
+
server.tool('windows_terminal_start', 'Start a persistent interactive terminal session (PowerShell, cmd, bash, python, node, wsl).', {
|
|
10
|
+
shell: z.enum(['pwsh', 'cmd', 'bash', 'python', 'node', 'wsl']).default('pwsh').describe('Shell type'),
|
|
11
|
+
}, async ({ shell }) => {
|
|
12
|
+
const session = startSession(shell);
|
|
13
|
+
// Give the shell a moment to start and produce initial output
|
|
14
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
15
|
+
return {
|
|
16
|
+
content: [{
|
|
17
|
+
type: 'text',
|
|
18
|
+
text: `Session started: PID ${session.pid} (${session.shell})\nUse windows_terminal_send to send commands, windows_terminal_read to read output.`,
|
|
19
|
+
}],
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
server.tool('windows_terminal_send', 'Send input to a running interactive terminal session.', {
|
|
23
|
+
pid: z.number().describe('Session PID'),
|
|
24
|
+
input: z.string().describe('Text to send to the session'),
|
|
25
|
+
}, async ({ pid, input }) => {
|
|
26
|
+
try {
|
|
27
|
+
sendToSession(pid, input);
|
|
28
|
+
// Wait for output to arrive
|
|
29
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
30
|
+
const lines = readSessionOutput(pid, -30);
|
|
31
|
+
return {
|
|
32
|
+
content: [{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: lines.join('\n') || '(no output yet)',
|
|
35
|
+
}],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return { content: [{ type: 'text', text: `Error: ${err}` }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
server.tool('windows_terminal_read', 'Read output from a terminal session with pagination. Use negative offset to read from the end.', {
|
|
43
|
+
pid: z.number().describe('Session PID'),
|
|
44
|
+
offset: z.number().default(0).describe('Line offset (negative = from end)'),
|
|
45
|
+
length: z.number().optional().describe('Number of lines to return'),
|
|
46
|
+
}, async ({ pid, offset, length }) => {
|
|
47
|
+
try {
|
|
48
|
+
const lines = readSessionOutput(pid, offset, length);
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: lines.join('\n') || '(no output)',
|
|
53
|
+
}],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return { content: [{ type: 'text', text: `Error: ${err}` }], isError: true };
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
server.tool('windows_terminal_list', 'List all active terminal sessions.', {}, async () => {
|
|
61
|
+
const sessions = listSessions();
|
|
62
|
+
if (sessions.length === 0) {
|
|
63
|
+
return { content: [{ type: 'text', text: 'No active sessions.' }] };
|
|
64
|
+
}
|
|
65
|
+
const lines = sessions.map(s => `PID ${s.pid} — ${s.shell} — ${s.running ? 'running' : 'ended'} — ${s.lines} lines — started ${s.startedAt}`);
|
|
66
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
67
|
+
});
|
|
68
|
+
server.tool('windows_terminal_kill', 'Terminate a terminal session by PID.', {
|
|
69
|
+
pid: z.number().describe('Session PID to terminate'),
|
|
70
|
+
}, async ({ pid }) => {
|
|
71
|
+
const killed = terminateSession(pid);
|
|
72
|
+
return {
|
|
73
|
+
content: [{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: killed ? `Session ${pid} terminated.` : `No session with PID ${pid}.`,
|
|
76
|
+
}],
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=terminal.js.map
|