From db62ae3d2590c0dca9534b6283cc08b2a537ec3f Mon Sep 17 00:00:00 2001 From: 1fanwang <1fannnw@gmail.com> Date: Tue, 8 Sep 2026 12:07:52 -0700 Subject: [PATCH 1/2] Validate the examples that follow an unvalidated language fence Closing a docs-validate hidden block sets a pending skip so the visible copy is not validated twice. A fence whose language has no validator, such as rust, was never recognized as a code block, so it never consumed that skip and the next validated block absorbed it instead. Signed-off-by: 1fanwang <1fannnw@gmail.com> --- scripts/docs-validation/extract.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts index df1c358deb..7ddd2c136c 100644 --- a/scripts/docs-validation/extract.ts +++ b/scripts/docs-validation/extract.ts @@ -61,6 +61,7 @@ function parseMarkdownCodeBlocks( let skipNext = false; let wrapAsync = false; let inHiddenBlock = false; + let inUnvalidatedFence = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; @@ -86,13 +87,25 @@ function parseMarkdownCodeBlocks( } // Start of code block - if (!inCodeBlock && line.startsWith("```")) { + if (!inCodeBlock && !inUnvalidatedFence && line.startsWith("```")) { const lang = line.slice(3).trim().toLowerCase(); if (lang && LANGUAGE_MAP[lang]) { inCodeBlock = true; currentLang = LANGUAGE_MAP[lang]; currentCode = []; blockStartLine = i + 1; // 1-indexed line number + } else { + inUnvalidatedFence = true; + } + continue; + } + + // End of a fence whose language has no validator. It still consumes a + // pending skip so the directive cannot leak onto a later block. + if (inUnvalidatedFence && line.startsWith("```")) { + inUnvalidatedFence = false; + if (!inHiddenBlock) { + skipNext = false; } continue; } From 50e9a214bf5f552da16fb0f65c76dc173160c6bf Mon Sep 17 00:00:00 2001 From: 1fanwang <1fannnw@gmail.com> Date: Tue, 8 Sep 2026 12:14:54 -0700 Subject: [PATCH 2/2] Document per-session plugin directories The plugin directories guide covered only the --plugin-dir launch argument and the trusted host-bundled startup option, so the per-session pluginDirectories config had no entry despite being typed in every language binding and forwarded on create and resume. Signed-off-by: 1fanwang <1fannnw@gmail.com> --- docs/features/plugin-directories.md | 45 +++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/features/plugin-directories.md b/docs/features/plugin-directories.md index 4af11d9a7e..456ef88e7f 100644 --- a/docs/features/plugin-directories.md +++ b/docs/features/plugin-directories.md @@ -240,6 +240,51 @@ let client = Client::start( > The example above uses an stdio runtime connection — the default when the SDK bundles the CLI. If you connect to an external runtime via a URL (`forUri` / `ForUri`), pass `--plugin-dir` to the long-running CLI server when you start it; the SDK does not forward `--plugin-dir` to runtimes it didn't spawn. +## Per-session plugin directories + +`--plugin-dir` is a launch argument, so it fixes one plugin set for the CLI process and every session created against it. When sessions need different plugin sets, or when the SDK is connected to a runtime it did not spawn, pass the directories on the session config instead. They travel in the `session.create` and `session.resume` payloads over JSON-RPC rather than as process arguments, so they reach an external runtime the same way the startup option does. + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +async function main() { + const client = new CopilotClient(); + await client.start(); + + const session = await client.createSession({ + pluginDirectories: ["./plugins/code-reviewer"], + }); +} + +main(); +``` + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.createSession({ + pluginDirectories: ["./plugins/code-reviewer"], +}); +``` + +Relative paths resolve against `workingDirectory`, or the runtime working directory when that is unset, so absolute paths are recommended. Entries that do not resolve are logged and skipped rather than failing session creation. The option is an explicit opt-in, which means plugin agents and rules load even when `enableConfigDiscovery` is false. Assets loaded this way sit between project sources and personal or home sources in the session-wide precedence order. + +The equivalent option in each SDK is: + +| SDK | Session option | +|---|---| +| Node.js / TypeScript | `pluginDirectories: string[]` | +| Python | `plugin_directories=[...]` | +| Go | `PluginDirectories: []string{...}` | +| .NET | `PluginDirectories = [...]` | +| Java | `.setPluginDirectories(List.of(...))` | +| Rust | `.with_plugin_directories([...])` | + ## Trusted host-bundled plugin directories Applications that ship their own trusted plugins can register them as a client startup option. The SDK sends the complete ordered set after connecting and verifying the protocol, before `start` returns or any session can be created. Paths must be absolute; leaving the option unset or empty makes no RPC call.