Skip to content

Commit 2e94454

Browse files
Add PowerShell language model tools
Register four read-only language model tools that Copilot can call when working with PowerShell, each backed by an existing PSES request so they report exactly what the user's session would: - `powershell_get_command` lists commands, optionally scoped by name and/or module, using the now-parameterized `powerShell/getCommand` request with `excludeParameters` so large listings stay cheap. - `powershell_get_help` returns `Get-Help` output via `powerShell/showHelp`. - `powershell_get_environment` reports the PowerShell version table. - `powershell_expand_alias` resolves aliases to their underlying commands. All four are read-only and declare no confirmation requirement. Contribute them in `package.json` and wire up `LanguageModelToolsFeature` in `extension.ts`. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d655273 commit 2e94454

4 files changed

Lines changed: 364 additions & 0 deletions

File tree

package.json

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,97 @@
125125
"language": "powershell"
126126
}
127127
],
128+
"languageModelTools": [
129+
{
130+
"name": "powershell_get_command",
131+
"toolReferenceName": "getPowerShellCommand",
132+
"displayName": "Get PowerShell Command",
133+
"modelDescription": "Get the commands (cmdlets, functions, and scripts) available in the user's active PowerShell session, scoped by name and/or module. You must provide a 'name' and/or 'module' filter (at least one is required). Returns each matching command's name, module, default parameter set, and parameter names. Use this to discover the exact name, module, and parameters of a PowerShell command instead of guessing.",
134+
"userDescription": "Lists commands available in the active PowerShell session.",
135+
"canBeReferencedInPrompt": true,
136+
"icon": "$(symbol-method)",
137+
"tags": [
138+
"powershell"
139+
],
140+
"inputSchema": {
141+
"type": "object",
142+
"properties": {
143+
"name": {
144+
"type": "string",
145+
"description": "Only return commands whose name matches this value (supports wildcards; bare text is matched as a substring), e.g. 'Get-ChildItem', 'ChildItem', or 'Get-*'. Provide this and/or 'module'."
146+
},
147+
"module": {
148+
"type": "string",
149+
"description": "Only return commands from this module (supports wildcards), e.g. 'Microsoft.PowerShell.Management'. Provide this and/or 'name'."
150+
}
151+
}
152+
}
153+
},
154+
{
155+
"name": "powershell_get_help",
156+
"toolReferenceName": "getPowerShellHelp",
157+
"displayName": "Get PowerShell Help",
158+
"modelDescription": "Get the full help (Get-Help -Full) for a specific PowerShell command from the user's active session, including synopsis, syntax, parameter descriptions, and examples. Use this to ground answers about how a PowerShell command works and what parameters it accepts, instead of guessing.",
159+
"userDescription": "Retrieves the full help for a PowerShell command.",
160+
"canBeReferencedInPrompt": true,
161+
"icon": "$(question)",
162+
"tags": [
163+
"powershell"
164+
],
165+
"inputSchema": {
166+
"type": "object",
167+
"properties": {
168+
"command": {
169+
"type": "string",
170+
"description": "The name of the PowerShell command to get help for, e.g. 'Get-ChildItem'."
171+
}
172+
},
173+
"required": [
174+
"command"
175+
]
176+
}
177+
},
178+
{
179+
"name": "powershell_get_environment",
180+
"toolReferenceName": "getPowerShellEnvironment",
181+
"displayName": "Get PowerShell Environment",
182+
"modelDescription": "Get details about the user's active PowerShell session, including the PowerShell version, edition (Core or Desktop), and process architecture. Use this before constructing version- or edition-specific PowerShell so that suggestions match the user's actual environment.",
183+
"userDescription": "Reports the active PowerShell version, edition, and architecture.",
184+
"canBeReferencedInPrompt": true,
185+
"icon": "$(terminal-powershell)",
186+
"tags": [
187+
"powershell"
188+
],
189+
"inputSchema": {
190+
"type": "object",
191+
"properties": {}
192+
}
193+
},
194+
{
195+
"name": "powershell_expand_alias",
196+
"toolReferenceName": "expandPowerShellAlias",
197+
"displayName": "Expand PowerShell Aliases",
198+
"modelDescription": "Expand the aliases in a PowerShell script to their full command names (for example 'gci' becomes 'Get-ChildItem' and '?' becomes 'Where-Object') using the user's active PowerShell session. Use this to normalize or clarify aliased PowerShell before explaining or editing it.",
199+
"userDescription": "Expands aliases in a PowerShell script to full command names.",
200+
"canBeReferencedInPrompt": true,
201+
"icon": "$(symbol-string)",
202+
"tags": [
203+
"powershell"
204+
],
205+
"inputSchema": {
206+
"type": "object",
207+
"properties": {
208+
"text": {
209+
"type": "string",
210+
"description": "The PowerShell script text whose aliases should be expanded."
211+
}
212+
},
213+
"required": [
214+
"text"
215+
]
216+
}
217+
}
218+
],
128219
"viewsContainers": {
129220
"activitybar": [
130221
{

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { GetCommandsFeature } from "./features/GetCommands";
2121
import { HelpCompletionFeature } from "./features/HelpCompletion";
2222
import { ISECompatibilityFeature } from "./features/ISECompatibility";
23+
import { LanguageModelToolsFeature } from "./features/LanguageModelTools";
2324
import { OpenInISEFeature } from "./features/OpenInISE";
2425
import { PesterTestsFeature } from "./features/PesterTests";
2526
import { RemoteFilesFeature } from "./features/RemoteFiles";
@@ -193,6 +194,7 @@ export async function activate(
193194
new RemoteFilesFeature(),
194195
new DebugSessionFeature(context, sessionManager, logger),
195196
new HelpCompletionFeature(),
197+
new LanguageModelToolsFeature(),
196198
];
197199

198200
sessionManager.setLanguageClientConsumers(languageClientConsumers);

src/features/LanguageModelTools.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import * as vscode from "vscode";
5+
import type { LanguageClient } from "vscode-languageclient/node";
6+
import { LanguageClientConsumer } from "../languageClientConsumer";
7+
import { PowerShellVersionRequestType } from "../session";
8+
import { ExpandAliasRequestType } from "./ExpandAlias";
9+
import { GetCommandRequestType } from "./GetCommands";
10+
import { ShowHelpRequestType } from "./ShowHelp";
11+
12+
function toToolResult(text: string): vscode.LanguageModelToolResult {
13+
return new vscode.LanguageModelToolResult([
14+
new vscode.LanguageModelTextPart(text),
15+
]);
16+
}
17+
18+
interface IGetCommandInput {
19+
name?: string;
20+
module?: string;
21+
}
22+
23+
// Lists commands available in the active PowerShell session (backed by the
24+
// existing powerShell/getCommand request), scoped by name and/or module. At
25+
// least one filter is required so we never serialize the entire command table,
26+
// which is prohibitively expensive.
27+
class GetCommandTool implements vscode.LanguageModelTool<IGetCommandInput> {
28+
public async invoke(
29+
options: vscode.LanguageModelToolInvocationOptions<IGetCommandInput>,
30+
token: vscode.CancellationToken,
31+
): Promise<vscode.LanguageModelToolResult> {
32+
const name = options.input.name?.trim();
33+
const module = options.input.module?.trim();
34+
35+
if (!name && !module) {
36+
return toToolResult(
37+
"Provide a 'name' and/or 'module' filter to look up PowerShell commands.",
38+
);
39+
}
40+
41+
// Get-Command -Name matches literally, so wrap bare text in wildcards to
42+
// get intuitive "contains" matching while leaving explicit wildcards
43+
// (and module names) untouched.
44+
const namePattern = name && !/[*?[\]]/.test(name) ? `*${name}*` : name;
45+
46+
const client = await LanguageClientConsumer.getLanguageClient();
47+
const matches = await client.sendRequest(
48+
GetCommandRequestType,
49+
{
50+
name: namePattern,
51+
module,
52+
},
53+
token,
54+
);
55+
56+
if (matches.length === 0) {
57+
return toToolResult(
58+
"No matching PowerShell commands were found in the current session.",
59+
);
60+
}
61+
62+
const limit = 50;
63+
const limited = matches.slice(0, limit);
64+
const blocks = limited.map((command) => {
65+
const parameters = Object.keys(command.parameters ?? {});
66+
return [
67+
`Name: ${command.name}`,
68+
`Module: ${command.moduleName || "(none)"}`,
69+
`DefaultParameterSet: ${command.defaultParameterSet ?? "(none)"}`,
70+
`Parameters: ${parameters.length > 0 ? parameters.join(", ") : "(none)"}`,
71+
].join("\n");
72+
});
73+
74+
let output = blocks.join("\n\n");
75+
if (matches.length > limit) {
76+
output += `\n\n(Showing ${limit} of ${matches.length} matching commands. Provide a more specific name or module filter to narrow the results.)`;
77+
}
78+
79+
return toToolResult(output);
80+
}
81+
}
82+
83+
interface IGetHelpInput {
84+
command: string;
85+
}
86+
87+
// A tool that takes no input.
88+
type EmptyInput = Record<string, never>;
89+
90+
// Returns the full Get-Help text for a command (backed by the powerShell/showHelp request).
91+
class GetHelpTool implements vscode.LanguageModelTool<IGetHelpInput> {
92+
public async invoke(
93+
options: vscode.LanguageModelToolInvocationOptions<IGetHelpInput>,
94+
token: vscode.CancellationToken,
95+
): Promise<vscode.LanguageModelToolResult> {
96+
const client = await LanguageClientConsumer.getLanguageClient();
97+
const result = await client.sendRequest(
98+
ShowHelpRequestType,
99+
{
100+
text: options.input.command,
101+
},
102+
token,
103+
);
104+
return toToolResult(
105+
result.helpText || `No help found for '${options.input.command}'.`,
106+
);
107+
}
108+
}
109+
110+
// Reports the active PowerShell version/edition/architecture (backed by powerShell/getVersion).
111+
class GetEnvironmentTool implements vscode.LanguageModelTool<EmptyInput> {
112+
public async invoke(
113+
_options: vscode.LanguageModelToolInvocationOptions<EmptyInput>,
114+
token: vscode.CancellationToken,
115+
): Promise<vscode.LanguageModelToolResult> {
116+
const client = await LanguageClientConsumer.getLanguageClient();
117+
const version = await client.sendRequest(
118+
PowerShellVersionRequestType,
119+
token,
120+
);
121+
const output = [
122+
`PowerShell version: ${version.version}`,
123+
`Edition: ${version.edition}`,
124+
`Architecture: ${version.architecture}`,
125+
`Commit: ${version.commit}`,
126+
].join("\n");
127+
return toToolResult(output);
128+
}
129+
}
130+
131+
interface IExpandAliasInput {
132+
text: string;
133+
}
134+
135+
// Expands aliases in a script to full command names (backed by powerShell/expandAlias).
136+
class ExpandAliasTool implements vscode.LanguageModelTool<IExpandAliasInput> {
137+
public async invoke(
138+
options: vscode.LanguageModelToolInvocationOptions<IExpandAliasInput>,
139+
token: vscode.CancellationToken,
140+
): Promise<vscode.LanguageModelToolResult> {
141+
const client = await LanguageClientConsumer.getLanguageClient();
142+
const result = await client.sendRequest(
143+
ExpandAliasRequestType,
144+
{
145+
text: options.input.text,
146+
},
147+
token,
148+
);
149+
return toToolResult(result.text);
150+
}
151+
}
152+
153+
export class LanguageModelToolsFeature extends LanguageClientConsumer {
154+
private tools: vscode.Disposable[];
155+
156+
constructor() {
157+
super();
158+
this.tools = [
159+
vscode.lm.registerTool(
160+
"powershell_get_command",
161+
new GetCommandTool(),
162+
),
163+
vscode.lm.registerTool("powershell_get_help", new GetHelpTool()),
164+
vscode.lm.registerTool(
165+
"powershell_get_environment",
166+
new GetEnvironmentTool(),
167+
),
168+
vscode.lm.registerTool(
169+
"powershell_expand_alias",
170+
new ExpandAliasTool(),
171+
),
172+
];
173+
}
174+
175+
public override onLanguageClientSet(
176+
_languageClient: LanguageClient,
177+
// eslint-disable-next-line @typescript-eslint/no-empty-function
178+
): void {}
179+
180+
public dispose(): void {
181+
for (const tool of this.tools) {
182+
tool.dispose();
183+
}
184+
}
185+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from "assert";
5+
import * as vscode from "vscode";
6+
import utils = require("../utils");
7+
8+
function getToolResultText(result: vscode.LanguageModelToolResult): string {
9+
return result.content
10+
.filter(
11+
(part): part is vscode.LanguageModelTextPart =>
12+
part instanceof vscode.LanguageModelTextPart,
13+
)
14+
.map((part) => part.value)
15+
.join("");
16+
}
17+
18+
async function invokeTool(name: string, input: object): Promise<string> {
19+
const result = await vscode.lm.invokeTool(name, {
20+
input,
21+
toolInvocationToken: undefined,
22+
});
23+
return getToolResultText(result);
24+
}
25+
26+
describe("Language model tools feature", function () {
27+
before(async function () {
28+
await utils.ensureEditorServicesIsConnected();
29+
});
30+
31+
const expectedTools = [
32+
"powershell_get_command",
33+
"powershell_get_help",
34+
"powershell_get_environment",
35+
"powershell_expand_alias",
36+
];
37+
38+
for (const name of expectedTools) {
39+
it(`Registers the ${name} tool`, function () {
40+
assert.ok(
41+
vscode.lm.tools.some((tool) => tool.name === name),
42+
`Expected tool '${name}' to be registered.`,
43+
);
44+
});
45+
}
46+
47+
it("Gets the PowerShell environment", async function () {
48+
const text = await invokeTool("powershell_get_environment", {});
49+
assert.match(text, /PowerShell version:/);
50+
assert.match(text, /Edition:/);
51+
});
52+
53+
it("Finds a command by name", async function () {
54+
const text = await invokeTool("powershell_get_command", {
55+
name: "Get-Command",
56+
});
57+
assert.match(text, /Get-Command/);
58+
});
59+
60+
it("Finds commands by module", async function () {
61+
const text = await invokeTool("powershell_get_command", {
62+
module: "Microsoft.PowerShell.Management",
63+
});
64+
assert.match(text, /Microsoft\.PowerShell\.Management/);
65+
});
66+
67+
it("Requires a filter for get_command", async function () {
68+
const text = await invokeTool("powershell_get_command", {});
69+
assert.match(text, /Provide a 'name' and\/or 'module' filter/);
70+
});
71+
72+
it("Gets help for a command", async function () {
73+
const text = await invokeTool("powershell_get_help", {
74+
command: "Get-Command",
75+
});
76+
assert.ok(text.length > 0, "Expected non-empty help text.");
77+
assert.match(text, /Get-Command/);
78+
});
79+
80+
it("Expands an alias", async function () {
81+
const text = await invokeTool("powershell_expand_alias", {
82+
text: "gci",
83+
});
84+
assert.match(text, /Get-ChildItem/);
85+
});
86+
});

0 commit comments

Comments
 (0)