@bexelbie/phone-a-friend 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brian Exelbierd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Phone a Friend
2
+
3
+ An MCP server that lets [GitHub Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) in Visual Studio Code dispatch work to a **different AI model** via [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli/about-github-copilot-in-the-cli). This not only allows diverse model usage but it also provides a path to getting gutter indicators for those changes.
4
+
5
+ ## The Problem
6
+
7
+ When you use GitHub Copilot Chat in VS Code, every subagent it spawns runs on the same model as the parent conversation. If you're on Claude Opus 4.6, all subagents are Claude Opus 4.6. Sometimes you want a different model for a subtask — a faster one for simple work, or a different vendor for a second opinion.
8
+
9
+ GitHub Copilot CLI supports `--model` to pick any available model, but using it directly doesn't help — changes made by the CLI don't produce VS Code's [gutter indicators](https://code.visualstudio.com/docs/sourcecontrol/staging-commits#_editor-gutter-indicators) (the green/blue/red diff decorations in the editor margin).
10
+
11
+ Phone a Friend solves this by returning a unified diff that the calling agent applies through VS Code's edit tools, giving you the same inline diff experience as if the changes were made natively.
12
+
13
+ ## How It Works
14
+
15
+ 1. GitHub Copilot Chat calls the `phone_a_friend` MCP tool with a prompt and model name
16
+ 2. The MCP server creates an isolated **git worktree** from the current repo's HEAD
17
+ 3. It launches GitHub Copilot CLI in non-interactive mode in that worktree with the requested model
18
+ 4. The subagent does its work and writes its response to a **message-in-a-bottle file** (`.paf-response.md`)
19
+ 5. The MCP server reads the response, captures a `git diff` of all changes, and cleans up the worktree
20
+ 6. Returns the response text and unified diff to the calling agent
21
+ 7. The calling agent applies the diff using VS Code's edit tools, producing gutter indicators
22
+
23
+ **Context cost warning:** The unified diff is returned as part of the tool result, which means it lands in the calling agent's context window. For large diffs this can be significant. Keep subtasks focused to keep diffs small.
24
+
25
+ ### Why "message in a bottle"?
26
+
27
+ GitHub Copilot CLI does not provide a way to retrieve just the agent's final response text. Its stdout mixes the response with progress output and is unreliable to parse. Rather than trying to extract a clean response from noisy output — and inflating the calling agent's context with the subagent's full thinking and execution log — we skip stdout entirely. The subagent writes its response to a file. We read the file.
28
+
29
+ ## Prerequisites
30
+
31
+ - **Node.js** >= 20.0.0
32
+ - **GitHub Copilot CLI** — installed, configured, and authenticated. See [About GitHub Copilot in the CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli/about-github-copilot-in-the-cli) for setup instructions.
33
+ - **Git** — for worktree management
34
+ - A **git repository** to work in (the tool operates on git repos)
35
+
36
+ Verify Copilot CLI is working:
37
+
38
+ ```bash
39
+ copilot --help
40
+ ```
41
+
42
+ ## Installation
43
+
44
+ ### From source
45
+
46
+ ```bash
47
+ git clone https://github.com/YOUR_USERNAME/phone-a-friend.git
48
+ cd phone-a-friend
49
+ npm install
50
+ npm run build
51
+ ```
52
+
53
+ ### As an npm package
54
+
55
+ ```bash
56
+ npm install -g @bexelbie/phone-a-friend
57
+ ```
58
+
59
+ ## Configuration
60
+
61
+ Add to your VS Code MCP configuration (`.vscode/mcp.json` in your workspace, or user-level via `MCP: Open User Configuration`):
62
+
63
+ ```json
64
+ {
65
+ "servers": {
66
+ "phoneAFriend": {
67
+ "type": "stdio",
68
+ "command": "node",
69
+ "args": ["/absolute/path/to/phone-a-friend/dist/index.js"]
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ Or if installed globally via npm:
76
+
77
+ ```json
78
+ {
79
+ "servers": {
80
+ "phoneAFriend": {
81
+ "type": "stdio",
82
+ "command": "phone-a-friend"
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ See [Use MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) for details on MCP server configuration.
89
+
90
+ ## Usage
91
+
92
+ Once configured, the `phone_a_friend` tool is available in GitHub Copilot Chat. You tell the agent what you want, and the agent decides how to use the tool — constructing the prompt, choosing the model, including context, and applying the returned diff.
93
+
94
+ ### Example prompts you might give Copilot Chat
95
+
96
+ Ask a different model for a code review:
97
+ ```
98
+ Use phone_a_friend with gpt-5 to review the changes in src/parser.ts and suggest improvements.
99
+ ```
100
+
101
+ Get a second opinion on architecture:
102
+ ```
103
+ Phone a friend using claude-sonnet-4.5 to evaluate whether our current database schema will scale to 100k users.
104
+ ```
105
+
106
+ Have a faster model do grunt work:
107
+ ```
108
+ Use phone_a_friend with gpt-5-mini to write unit tests for all public functions in src/utils.ts.
109
+ ```
110
+
111
+ ### How agents discover the tool
112
+
113
+ The MCP server exposes the tool name, description, and parameter schemas to the calling agent via the MCP protocol. The tool description includes "when to use" and "when not to use" guidance so agents can decide whether `phone_a_friend` is appropriate for a given task.
114
+
115
+ **Interaction patterns that work:**
116
+ - **Explicit tool name**: "Use phone_a_friend with gpt-5 to..." — always works
117
+ - **Natural name**: "Phone a friend using claude-sonnet-4.5 to..." — agents match the tool name
118
+ - **Model-oriented**: "Ask gpt-5 to review this code" or "Get a second opinion from claude-sonnet-4" — agents recognize the request for a different model and match it to the tool
119
+ - **Keyword triggers**: Mentioning "subagent", "different model", or "second opinion" in a coding context — the tool description tells agents to look for these signals
120
+
121
+ **Patterns that probably won't trigger the tool automatically:**
122
+ - Vague delegation like "use subagents where appropriate" — the agent may not proactively reach for this tool without a specific model or second-opinion request
123
+ - Tasks that the current model can handle directly — agents generally prefer their own capabilities over adding a tool-call round trip
124
+
125
+ If you want the agent to consistently use this tool, be specific: name the tool, name the model, or clearly ask for a different model's perspective.
126
+
127
+ ### What the calling agent is responsible for
128
+
129
+ The tool description tells the calling agent what it needs to know, but in short:
130
+
131
+ - **Prompt construction**: The subagent only sees committed files. If the user's request involves uncommitted work, the calling agent must include the relevant file contents in the prompt.
132
+ - **Working directory**: The calling agent must always pass `working_directory` explicitly, using the workspace path from the conversation context. The MCP server's own working directory is not the user's workspace.
133
+ - **Diff application**: The tool returns a unified diff. The calling agent applies it using its own edit tools to produce gutter indicators.
134
+ - **Context management**: The diff lands in the calling agent's context window. The calling agent should keep subtasks focused to avoid large diffs.
135
+
136
+ ### Available models
137
+
138
+ The following models are available as of the latest release of this project. We try to check periodically, but if you discover models are missing, please open an issue or PR.
139
+
140
+ - `claude-sonnet-4.5`, `claude-haiku-4.5`, `claude-opus-4.6`, `claude-opus-4.6-fast`, `claude-opus-4.5`, `claude-sonnet-4`
141
+ - `gemini-3-pro-preview`
142
+ - `gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5.1-codex`, `gpt-5.1`, `gpt-5`, `gpt-5.1-codex-mini`, `gpt-5-mini`, `gpt-4.1`
143
+
144
+ You can direct the model to pass any model name directly — GitHub Copilot CLI validates it at runtime.
145
+
146
+ ## Tool Parameters
147
+
148
+ | Parameter | Required | Description |
149
+ |-----------|----------|-------------|
150
+ | `prompt` | Yes | The task or question for the other model. Include relevant context. |
151
+ | `model` | Yes | Which AI model to use (see list above). |
152
+ | `working_directory` | Yes | Git repo directory to work in. Must be inside a git repository. Always pass this explicitly — the server's own working directory is not the user's workspace. |
153
+
154
+ ## Safety
155
+
156
+ - **Push protection**: The CLI is invoked with `--deny-tool 'shell(git push*)'` which blocks all push attempts at the tool level. The prompt also instructs the agent not to push.
157
+ - **Worktree isolation**: All work happens in a temporary git worktree. Your working tree is never modified directly.
158
+ - **Automatic cleanup**: Worktrees are removed after each invocation, even on errors.
159
+ - **No secrets exposure**: The subagent has the same access as `copilot` CLI running locally — no additional permissions are granted.
160
+
161
+ ## Known Limitations
162
+
163
+ - **Blocks the calling agent**: MCP tool calls are synchronous — this is a property of the MCP protocol, not this tool. The calling agent waits for the subagent to finish before it can do anything else. Keep subtasks focused.
164
+ - **Uncommitted changes**: The worktree is created from HEAD. Uncommitted changes in your working tree are not visible to the subagent. The calling agent's tool description instructs it to include relevant file contents in the prompt when needed.
165
+ - **Message-in-a-bottle compliance**: The subagent must follow instructions to write `.paf-response.md`. Most models do, but some may occasionally ignore the instruction.
166
+ - **No streaming**: There is nothing to stream — the response is a file and a diff, both captured after the subagent finishes.
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ npm install
172
+ npm run build # Compile TypeScript
173
+ npm test # Run tests
174
+ npm run dev # Watch mode for development
175
+ ```
176
+
177
+ ## License
178
+
179
+ MIT
package/dist/git.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Verifies that a directory is inside a git repository.
3
+ */
4
+ export declare function isGitRepo(dir: string): Promise<boolean>;
5
+ /**
6
+ * Gets the root of the git repository containing the given directory.
7
+ */
8
+ export declare function getGitRoot(dir: string): Promise<string>;
9
+ /**
10
+ * Creates a git worktree at the specified path, branching from HEAD.
11
+ */
12
+ export declare function createWorktree(repoDir: string, worktreePath: string): Promise<void>;
13
+ /**
14
+ * Removes a git worktree and its associated branch.
15
+ */
16
+ export declare function removeWorktree(repoDir: string, worktreePath: string): Promise<void>;
17
+ /**
18
+ * Runs the Copilot CLI in non-interactive mode inside the worktree.
19
+ * Stdout/stderr are discarded — the response comes from the
20
+ * message-in-a-bottle file.
21
+ */
22
+ export declare function runCopilotCli(worktreePath: string, prompt: string, model: string): Promise<{
23
+ exitCode: number;
24
+ stderr: string;
25
+ }>;
26
+ /**
27
+ * Returns the current HEAD commit SHA for a git directory.
28
+ */
29
+ export declare function getHeadSha(dir: string): Promise<string>;
30
+ /**
31
+ * Captures all changes in the worktree as a unified diff.
32
+ * Stages everything first to include untracked files, then diffs
33
+ * against the given base SHA (the original commit before the agent ran).
34
+ * This ensures committed changes are also captured.
35
+ */
36
+ export declare function captureChanges(worktreePath: string, baseSha: string): Promise<string>;
37
+ /**
38
+ * Reads and removes the message-in-a-bottle response file.
39
+ */
40
+ export declare function readAndRemoveResponse(worktreePath: string, responseFilename: string): Promise<string | null>;
41
+ //# sourceMappingURL=git.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO7D;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAwC/C;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAkBjB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
package/dist/git.js ADDED
@@ -0,0 +1,152 @@
1
+ // ABOUTME: Git operations for worktree management, repo inspection, and change capture.
2
+ // ABOUTME: Extracted from index.ts so these functions can be tested directly.
3
+ import { execFile, spawn } from "node:child_process";
4
+ import { readFile, unlink, mkdir, rm } from "node:fs/promises";
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { promisify } from "node:util";
8
+ const execFileAsync = promisify(execFile);
9
+ /**
10
+ * Verifies that a directory is inside a git repository.
11
+ */
12
+ export async function isGitRepo(dir) {
13
+ try {
14
+ await execFileAsync("git", ["rev-parse", "--git-dir"], { cwd: dir });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Gets the root of the git repository containing the given directory.
23
+ */
24
+ export async function getGitRoot(dir) {
25
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd: dir });
26
+ return stdout.trim();
27
+ }
28
+ /**
29
+ * Creates a git worktree at the specified path, branching from HEAD.
30
+ */
31
+ export async function createWorktree(repoDir, worktreePath) {
32
+ const worktreesDir = join(worktreePath, "..");
33
+ if (!existsSync(worktreesDir)) {
34
+ await mkdir(worktreesDir, { recursive: true });
35
+ }
36
+ const branchName = `paf/${worktreePath.split("/").pop()}`;
37
+ await execFileAsync("git", ["worktree", "add", "-B", branchName, worktreePath, "HEAD"], { cwd: repoDir });
38
+ }
39
+ /**
40
+ * Removes a git worktree and its associated branch.
41
+ */
42
+ export async function removeWorktree(repoDir, worktreePath) {
43
+ const branchName = `paf/${worktreePath.split("/").pop()}`;
44
+ try {
45
+ await execFileAsync("git", ["worktree", "remove", worktreePath, "--force"], { cwd: repoDir });
46
+ }
47
+ catch {
48
+ // If worktree remove fails, try manual cleanup
49
+ if (existsSync(worktreePath)) {
50
+ await rm(worktreePath, { recursive: true, force: true });
51
+ }
52
+ try {
53
+ await execFileAsync("git", ["worktree", "prune"], { cwd: repoDir });
54
+ }
55
+ catch {
56
+ // Best effort
57
+ }
58
+ }
59
+ try {
60
+ await execFileAsync("git", ["branch", "-D", branchName], {
61
+ cwd: repoDir,
62
+ });
63
+ }
64
+ catch {
65
+ // Branch may not exist or may already be deleted
66
+ }
67
+ }
68
+ /**
69
+ * Runs the Copilot CLI in non-interactive mode inside the worktree.
70
+ * Stdout/stderr are discarded — the response comes from the
71
+ * message-in-a-bottle file.
72
+ */
73
+ export async function runCopilotCli(worktreePath, prompt, model) {
74
+ return new Promise((resolvePromise) => {
75
+ const args = [
76
+ "-p",
77
+ prompt,
78
+ "--model",
79
+ model,
80
+ "--allow-all",
81
+ "--deny-tool",
82
+ "shell(git push*)",
83
+ "--no-alt-screen",
84
+ "--no-color",
85
+ ];
86
+ const child = spawn("copilot", args, {
87
+ cwd: worktreePath,
88
+ stdio: ["ignore", "pipe", "pipe"],
89
+ env: {
90
+ ...process.env,
91
+ NO_COLOR: "1",
92
+ TERM: "dumb",
93
+ },
94
+ });
95
+ let stderr = "";
96
+ child.stderr?.on("data", (data) => {
97
+ stderr += data.toString();
98
+ });
99
+ // Drain stdout so the process doesn't block
100
+ child.stdout?.on("data", () => { });
101
+ child.on("close", (code) => {
102
+ resolvePromise({ exitCode: code ?? 1, stderr });
103
+ });
104
+ child.on("error", (err) => {
105
+ resolvePromise({ exitCode: 1, stderr: err.message });
106
+ });
107
+ });
108
+ }
109
+ /**
110
+ * Returns the current HEAD commit SHA for a git directory.
111
+ */
112
+ export async function getHeadSha(dir) {
113
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: dir });
114
+ return stdout.trim();
115
+ }
116
+ /**
117
+ * Captures all changes in the worktree as a unified diff.
118
+ * Stages everything first to include untracked files, then diffs
119
+ * against the given base SHA (the original commit before the agent ran).
120
+ * This ensures committed changes are also captured.
121
+ */
122
+ export async function captureChanges(worktreePath, baseSha) {
123
+ // Stage everything so untracked files appear in the diff
124
+ try {
125
+ await execFileAsync("git", ["add", "-A"], { cwd: worktreePath });
126
+ }
127
+ catch {
128
+ // May fail if nothing to add
129
+ }
130
+ try {
131
+ const { stdout } = await execFileAsync("git", ["diff", "--staged", baseSha], { cwd: worktreePath, maxBuffer: 10 * 1024 * 1024 });
132
+ return stdout;
133
+ }
134
+ catch {
135
+ return "";
136
+ }
137
+ }
138
+ /**
139
+ * Reads and removes the message-in-a-bottle response file.
140
+ */
141
+ export async function readAndRemoveResponse(worktreePath, responseFilename) {
142
+ const responsePath = join(worktreePath, responseFilename);
143
+ try {
144
+ const content = await readFile(responsePath, "utf-8");
145
+ await unlink(responsePath);
146
+ return content;
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ // ABOUTME: MCP server that invokes Copilot CLI with a different model.
3
+ // ABOUTME: Enables cross-model subagent calls from VS Code Copilot Chat.
4
+ import { resolve } from "node:path";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import { RESPONSE_FILENAME, AVAILABLE_MODELS, wrapPrompt, generateWorktreePath, promptSizeWarning, } from "./util.js";
9
+ import { isGitRepo, getGitRoot, createWorktree, removeWorktree, runCopilotCli, getHeadSha, captureChanges, readAndRemoveResponse, } from "./git.js";
10
+ // --- MCP Server Setup ---
11
+ const server = new McpServer({
12
+ name: "phone-a-friend",
13
+ version: "0.1.0",
14
+ });
15
+ server.registerTool("phone_a_friend", {
16
+ title: "Phone a Friend",
17
+ description: "Invoke GitHub Copilot CLI with a different AI model to perform a task. " +
18
+ "The other model works in an isolated git worktree. Returns the model's " +
19
+ "response message and a unified diff of any file changes it made. " +
20
+ "You can then apply the diff using your own edit tools to give the user " +
21
+ "inline diff highlighting." +
22
+ "\n\nWhen to use this tool:\n" +
23
+ "- The user asks for a second opinion or review from a different model\n" +
24
+ "- The user wants a specific model for a subtask (e.g., a faster model for simple work, or a different vendor)\n" +
25
+ "- The user mentions \"phone a friend\", \"subagent\", or \"different model\"\n" +
26
+ "- The user wants to delegate a focused coding task to another model and get the changes back as a diff\n" +
27
+ "\n\nWhen NOT to use this tool:\n" +
28
+ "- The current model can handle the task directly — don't add round-trip overhead for no benefit\n" +
29
+ "- The task requires seeing uncommitted changes and the user hasn't provided the file contents\n" +
30
+ "- The task is conversational (no code changes expected and no specialized model needed)\n" +
31
+ "\n\nIMPORTANT: The subagent only sees committed files (HEAD). It cannot " +
32
+ "see uncommitted changes in the working tree. If the user's request " +
33
+ "involves uncommitted work, YOU must include the relevant file contents " +
34
+ "in the prompt. The diff returned will be in your context — keep " +
35
+ "subtasks focused to avoid large diffs consuming your context window.",
36
+ inputSchema: z.object({
37
+ prompt: z
38
+ .string()
39
+ .describe("The task or question for the other model. Be specific and include " +
40
+ "relevant context since the other model starts with only the " +
41
+ "committed repository files. If the user has uncommitted changes " +
42
+ "that are relevant, include those file contents in this prompt."),
43
+ model: z
44
+ .string()
45
+ .describe(`The AI model to use. Available models: ${AVAILABLE_MODELS.join(", ")}`),
46
+ working_directory: z
47
+ .string()
48
+ .describe("The git repository directory to work in. Must be inside a git " +
49
+ "repository. Always pass this explicitly using the workspace " +
50
+ "path from your conversation context — the server's own working " +
51
+ "directory is not the user's workspace."),
52
+ }),
53
+ }, async ({ prompt, model, working_directory }) => {
54
+ const workDir = resolve(working_directory);
55
+ // Validate git repo
56
+ if (!(await isGitRepo(workDir))) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text",
61
+ text: `Error: ${workDir} is not inside a git repository.`,
62
+ },
63
+ ],
64
+ isError: true,
65
+ };
66
+ }
67
+ const gitRoot = await getGitRoot(workDir);
68
+ const worktreePath = generateWorktreePath(gitRoot);
69
+ try {
70
+ // Create isolated worktree
71
+ await createWorktree(gitRoot, worktreePath);
72
+ // Record the starting commit so we can diff against it later,
73
+ // even if the agent commits during its run.
74
+ const baseSha = await getHeadSha(worktreePath);
75
+ // Run the CLI agent
76
+ const wrappedPrompt = wrapPrompt(prompt);
77
+ const { exitCode, stderr } = await runCopilotCli(worktreePath, wrappedPrompt, model);
78
+ // Read the response file before diffing
79
+ const response = await readAndRemoveResponse(worktreePath, RESPONSE_FILENAME);
80
+ // Capture all file changes
81
+ const diff = await captureChanges(worktreePath, baseSha);
82
+ // Build result
83
+ const parts = [];
84
+ if (response) {
85
+ parts.push("## Agent Response\n\n" + response);
86
+ }
87
+ else {
88
+ parts.push("## Agent Response\n\n" +
89
+ "*No response file was created. The agent may have failed " +
90
+ "to follow the message-in-a-bottle instructions.*");
91
+ }
92
+ if (diff.trim()) {
93
+ parts.push("## File Changes (unified diff)\n\n```diff\n" + diff + "\n```");
94
+ }
95
+ else {
96
+ parts.push("## File Changes\n\nNo file changes were made.");
97
+ }
98
+ if (exitCode !== 0) {
99
+ parts.push(`## Warnings\n\nCopilot CLI exited with code ${exitCode}.` +
100
+ (stderr ? `\n\nStderr:\n${stderr}` : ""));
101
+ }
102
+ const sizeWarning = promptSizeWarning(prompt);
103
+ if (sizeWarning) {
104
+ parts.push("## Context Size Notice\n\n" + sizeWarning);
105
+ }
106
+ return {
107
+ content: [{ type: "text", text: parts.join("\n\n") }],
108
+ };
109
+ }
110
+ finally {
111
+ // Always clean up the worktree
112
+ try {
113
+ await removeWorktree(gitRoot, worktreePath);
114
+ }
115
+ catch {
116
+ // Best effort cleanup
117
+ }
118
+ }
119
+ });
120
+ // Start the server
121
+ const transport = new StdioServerTransport();
122
+ await server.connect(transport);
123
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/util.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export declare const RESPONSE_FILENAME = ".paf-response.md";
2
+ export declare const AVAILABLE_MODELS: readonly ["claude-sonnet-4.5", "claude-haiku-4.5", "claude-opus-4.6", "claude-opus-4.6-fast", "claude-opus-4.5", "claude-sonnet-4", "gemini-3-pro-preview", "gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.2", "gpt-5.1-codex-max", "gpt-5.1-codex", "gpt-5.1", "gpt-5", "gpt-5.1-codex-mini", "gpt-5-mini", "gpt-4.1"];
3
+ /**
4
+ * Wraps the user's prompt with instructions for the subagent about
5
+ * the message-in-a-bottle response file and push restrictions.
6
+ */
7
+ export declare function wrapPrompt(userPrompt: string): string;
8
+ /**
9
+ * Generates a unique worktree path based on timestamp and random suffix.
10
+ */
11
+ export declare function generateWorktreePath(baseDir: string): string;
12
+ /**
13
+ * Escapes a string for safe shell inclusion.
14
+ */
15
+ export declare function shellEscape(s: string): string;
16
+ export declare const PROMPT_SIZE_WARNING_THRESHOLD: number;
17
+ /**
18
+ * Returns a warning string if the prompt is large enough to suggest
19
+ * the calling agent pasted file contents (likely for uncommitted changes).
20
+ * Returns null if the prompt is under the threshold.
21
+ */
22
+ export declare function promptSizeWarning(prompt: string): string | null;
23
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,iBAAiB,qBAAqB,CAAC;AAEpD,eAAO,MAAM,gBAAgB,mTAkBnB,CAAC;AAEX;;;GAGG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAgBrD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAI5D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAIvD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY/D"}
package/dist/util.js ADDED
@@ -0,0 +1,77 @@
1
+ // ABOUTME: Pure utility functions for prompt wrapping and path generation.
2
+ // ABOUTME: Separated from server startup so tests can import without side effects.
3
+ import { join } from "node:path";
4
+ export const RESPONSE_FILENAME = ".paf-response.md";
5
+ export const AVAILABLE_MODELS = [
6
+ "claude-sonnet-4.5",
7
+ "claude-haiku-4.5",
8
+ "claude-opus-4.6",
9
+ "claude-opus-4.6-fast",
10
+ "claude-opus-4.5",
11
+ "claude-sonnet-4",
12
+ "gemini-3-pro-preview",
13
+ "gpt-5.3-codex",
14
+ "gpt-5.2-codex",
15
+ "gpt-5.2",
16
+ "gpt-5.1-codex-max",
17
+ "gpt-5.1-codex",
18
+ "gpt-5.1",
19
+ "gpt-5",
20
+ "gpt-5.1-codex-mini",
21
+ "gpt-5-mini",
22
+ "gpt-4.1",
23
+ ];
24
+ /**
25
+ * Wraps the user's prompt with instructions for the subagent about
26
+ * the message-in-a-bottle response file and push restrictions.
27
+ */
28
+ export function wrapPrompt(userPrompt) {
29
+ return `## CRITICAL INSTRUCTIONS — READ BEFORE DOING ANYTHING
30
+
31
+ You are operating as a subagent inside an isolated git worktree. Follow these rules:
32
+
33
+ 1. **NEVER push to any remote.** Do not run \`git push\` under any circumstances.
34
+ 2. **Commits are fine.** You may commit freely within this worktree.
35
+ 3. **When you are finished**, write your complete final response to the file \`${RESPONSE_FILENAME}\` in the repository root. This file is your ONLY communication channel back to the calling agent. Include:
36
+ - A summary of what you did
37
+ - Any important findings or decisions
38
+ - Any warnings or caveats
39
+ 4. You MUST create the \`${RESPONSE_FILENAME}\` file even if you made no code changes. It is how you report back.
40
+
41
+ ## YOUR TASK
42
+
43
+ ${userPrompt}`;
44
+ }
45
+ /**
46
+ * Generates a unique worktree path based on timestamp and random suffix.
47
+ */
48
+ export function generateWorktreePath(baseDir) {
49
+ const timestamp = Date.now();
50
+ const suffix = Math.random().toString(36).substring(2, 8);
51
+ return join(baseDir, `.worktrees`, `paf-${timestamp}-${suffix}`);
52
+ }
53
+ /**
54
+ * Escapes a string for safe shell inclusion.
55
+ */
56
+ export function shellEscape(s) {
57
+ return `'${s.replace(/'/g, "'\\''")}'`;
58
+ }
59
+ export const PROMPT_SIZE_WARNING_THRESHOLD = 10 * 1024; // 10 KB
60
+ const REPO_ISSUES_URL = "https://github.com/bexelbie/phone-a-friend/issues";
61
+ /**
62
+ * Returns a warning string if the prompt is large enough to suggest
63
+ * the calling agent pasted file contents (likely for uncommitted changes).
64
+ * Returns null if the prompt is under the threshold.
65
+ */
66
+ export function promptSizeWarning(prompt) {
67
+ if (prompt.length < PROMPT_SIZE_WARNING_THRESHOLD) {
68
+ return null;
69
+ }
70
+ const sizeKB = Math.round(prompt.length / 1024);
71
+ return (`**Large prompt warning:** The prompt sent to the subagent was ~${sizeKB}KB. ` +
72
+ `If this included pasted file contents to work around the uncommitted changes ` +
73
+ `limitation, be aware this consumes significant context in both directions. ` +
74
+ `If this is causing problems, consider requesting built-in uncommitted changes ` +
75
+ `support: ${REPO_ISSUES_URL}`);
76
+ }
77
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,mFAAmF;AAEnF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAEpD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,mBAAmB;IACnB,kBAAkB;IAClB,iBAAiB;IACjB,sBAAsB;IACtB,iBAAiB;IACjB,iBAAiB;IACjB,sBAAsB;IACtB,eAAe;IACf,eAAe;IACf,SAAS;IACT,mBAAmB;IACnB,eAAe;IACf,SAAS;IACT,OAAO;IACP,oBAAoB;IACpB,YAAY;IACZ,SAAS;CACD,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,UAAkB;IAC3C,OAAO;;;;;;iFAMwE,iBAAiB;;;;2BAIvE,iBAAiB;;;;EAI1C,UAAU,EAAE,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAe;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1D,OAAO,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,SAAS,IAAI,MAAM,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,CAAS;IACnC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AACzC,CAAC;AAED,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,QAAQ;AAEhE,MAAM,eAAe,GAAG,mDAAmD,CAAC;AAE5E;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,IAAI,MAAM,CAAC,MAAM,GAAG,6BAA6B,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,CACL,kEAAkE,MAAM,MAAM;QAC9E,+EAA+E;QAC/E,6EAA6E;QAC7E,gFAAgF;QAChF,YAAY,eAAe,EAAE,CAC9B,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@bexelbie/phone-a-friend",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that lets VS Code Copilot Chat invoke Copilot CLI with a different model",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "phone-a-friend": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "!dist/test/"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsc --watch",
18
+ "test": "node --test dist/test/*.test.js",
19
+ "pretest": "tsc"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "copilot",
24
+ "github-copilot",
25
+ "multi-model",
26
+ "subagent"
27
+ ],
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=20.0.0"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.26.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "typescript": "^5.7.0"
38
+ }
39
+ }