pi-repoprompt-mcp 0.5.1 → 0.5.3

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/README.md CHANGED
@@ -196,6 +196,8 @@ Options:
196
196
  | `diffViewMode` | `"auto"` | Diff layout for RepoPrompt `git` / `apply_edits` fenced diff output (`auto`, `split`, `unified`) |
197
197
  | `diffSplitMinWidth` | `120` | Minimum render width before `diffViewMode: "auto"` uses split diff layout |
198
198
  | `suppressHostDisconnectedLog` | `true` | Filter noisy stderr from macOS `repoprompt-mcp` (disconnect/retry bootstrap logs) |
199
+ | `autoLaunchApp` | `false` | Auto-launch the RepoPrompt app when the MCP server is unreachable at startup |
200
+ | `appPath` | inferred | Explicit path to `Repo Prompt.app`; if omitted, inferred from the `.app` ancestor of `command` |
199
201
 
200
202
  Automatic tab restoration and provisioning is driven by `autoBindOnStart` and `persistBinding`; there is no separate tab-only configuration surface. Adaptive diff layout applies only to RepoPrompt `git` and `apply_edits` outputs that arrive as fenced `diff` blocks; other rendered output stays on the existing text-based path.
201
203
 
@@ -219,6 +221,10 @@ but this is best-effort
219
221
  ### Pi becomes unresponsive after closing/restarting RepoPrompt
220
222
  If the RepoPrompt MCP server stops responding (for example, if the RepoPrompt app is closed while Pi stays open), tool calls may time out. When that happens, the extension will drop the connection and you can recover with `/rp reconnect`.
221
223
 
224
+ If RepoPrompt is not running when Pi starts, the extension auto-pauses itself after a quick connection timeout. While paused, the `rp` tool returns a short error directing the agent to use native tools. Run `/rp reconnect` once RepoPrompt is open to resume, and the agent will be notified that `rp` is available again.
225
+
226
+ If `autoLaunchApp` is enabled, the extension will try to open the RepoPrompt app automatically before pausing. The app path is inferred from the `command` config (e.g. `/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp` → `/Applications/Repo Prompt.app`), or you can set `appPath` explicitly. After launching, the extension waits a few seconds and retries the connection once; if that also fails, it auto-pauses as usual.
227
+
222
228
  ### "No matching window found"
223
229
  - Your `cwd` may not match any RepoPrompt workspace root
224
230
  - Use `/rp windows` to list windows
@@ -15,12 +15,32 @@ const CLIENT_INFO = {
15
15
  version: "1.0.0",
16
16
  };
17
17
 
18
+ const DEFAULT_CONNECT_TIMEOUT_MS = 6_000;
18
19
  const DEFAULT_LIST_TOOLS_TIMEOUT_MS = 10_000;
19
20
 
20
21
  // Keep parity with the rp-cli integration default (15 minutes). Some RepoPrompt tools
21
22
  // (notably context_builder and chat_send) can legitimately take longer than 10s
22
23
  const DEFAULT_TOOL_CALL_TIMEOUT_MS = 15 * 60 * 1000;
23
24
 
25
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
26
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
27
+
28
+ try {
29
+ return await Promise.race([
30
+ promise,
31
+ new Promise<T>((_, reject) => {
32
+ timeoutId = setTimeout(() => {
33
+ reject(new Error(message));
34
+ }, timeoutMs);
35
+ }),
36
+ ]);
37
+ } finally {
38
+ if (timeoutId) {
39
+ clearTimeout(timeoutId);
40
+ }
41
+ }
42
+ }
43
+
24
44
 
25
45
  /**
26
46
  * Manages the MCP connection to RepoPrompt server
@@ -31,23 +51,23 @@ export class RpClient {
31
51
  private _status: ConnectionStatus = "disconnected";
32
52
  private _tools: RpToolMeta[] = [];
33
53
  private _error: string | undefined;
34
-
54
+
35
55
  get status(): ConnectionStatus {
36
56
  return this._status;
37
57
  }
38
-
58
+
39
59
  get tools(): RpToolMeta[] {
40
60
  return this._tools;
41
61
  }
42
-
62
+
43
63
  get error(): string | undefined {
44
64
  return this._error;
45
65
  }
46
-
66
+
47
67
  get isConnected(): boolean {
48
68
  return this._status === "connected" && this.client !== null;
49
69
  }
50
-
70
+
51
71
  /**
52
72
  * Connect to the RepoPrompt MCP server
53
73
  */
@@ -59,13 +79,13 @@ export class RpClient {
59
79
  if (this._status === "connecting") {
60
80
  throw new Error("Connection already in progress");
61
81
  }
62
-
82
+
63
83
  // Close existing connection if any
64
84
  await this.close();
65
-
85
+
66
86
  this._status = "connecting";
67
87
  this._error = undefined;
68
-
88
+
69
89
  try {
70
90
  // Create transport
71
91
  const mergedEnv: Record<string, string> = {};
@@ -77,36 +97,40 @@ export class RpClient {
77
97
  if (env) {
78
98
  Object.assign(mergedEnv, env);
79
99
  }
80
-
100
+
81
101
  this.transport = new StdioClientTransport({
82
102
  command,
83
103
  args,
84
104
  env: Object.keys(mergedEnv).length > 0 ? mergedEnv : undefined,
85
105
  });
86
-
106
+
87
107
  // Create client
88
108
  this.client = new Client(CLIENT_INFO, {
89
109
  capabilities: {},
90
110
  });
91
-
111
+
92
112
  // Connect
93
- await this.client.connect(this.transport);
94
-
113
+ await withTimeout(
114
+ this.client.connect(this.transport),
115
+ DEFAULT_CONNECT_TIMEOUT_MS,
116
+ `Timed out connecting to RepoPrompt MCP server after ${DEFAULT_CONNECT_TIMEOUT_MS}ms`
117
+ );
118
+
95
119
  // Fetch available tools
96
120
  await this.refreshTools();
97
-
121
+
98
122
  this._status = "connected";
99
123
  } catch (error) {
100
124
  this._status = "error";
101
125
  this._error = error instanceof Error ? error.message : String(error);
102
-
126
+
103
127
  // Clean up on error
104
128
  await this.close();
105
-
129
+
106
130
  throw error;
107
131
  }
108
132
  }
109
-
133
+
110
134
  /**
111
135
  * Refresh the list of available tools
112
136
  */
@@ -125,7 +149,7 @@ export class RpClient {
125
149
 
126
150
  return this._tools;
127
151
  }
128
-
152
+
129
153
  /**
130
154
  * Call a tool on the RepoPrompt MCP server
131
155
  */
@@ -187,33 +211,38 @@ export class RpClient {
187
211
  isError: Boolean(result.isError),
188
212
  };
189
213
  }
190
-
214
+
191
215
  /**
192
216
  * Close the connection
193
217
  */
194
218
  async close(): Promise<void> {
195
- if (this.client) {
219
+ const client = this.client;
220
+ const transport = this.transport;
221
+ const wasConnecting = this._status === "connecting";
222
+
223
+ this.client = null;
224
+ this.transport = null;
225
+ this._status = "disconnected";
226
+ this._tools = [];
227
+
228
+ // If connect() never completed, skip the graceful MCP close and tear down the transport directly
229
+ if (client && !wasConnecting) {
196
230
  try {
197
- await this.client.close();
231
+ await client.close();
198
232
  } catch {
199
233
  // Ignore close errors
200
234
  }
201
- this.client = null;
202
235
  }
203
236
 
204
- if (this.transport) {
237
+ if (transport) {
205
238
  try {
206
- await this.transport.close();
239
+ await transport.close();
207
240
  } catch {
208
241
  // Ignore close errors
209
242
  }
210
- this.transport = null;
211
243
  }
212
-
213
- this._status = "disconnected";
214
- this._tools = [];
215
244
  }
216
-
245
+
217
246
  /**
218
247
  * Get connection info for debugging
219
248
  */
@@ -221,7 +250,7 @@ export class RpClient {
221
250
  if (!this.client || !this.transport) {
222
251
  return null;
223
252
  }
224
-
253
+
225
254
  return {
226
255
  client: this.client,
227
256
  transport: this.transport,
@@ -250,6 +250,21 @@ function maybeWrapServerCommand(
250
250
  };
251
251
  }
252
252
 
253
+ /**
254
+ * Infer the .app bundle path from an MCP server command that lives inside a .app bundle.
255
+ * e.g. "/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp" → "/Applications/Repo Prompt.app"
256
+ */
257
+ export function inferAppPath(config: RpConfig): string | null {
258
+ if (config.appPath) {
259
+ return config.appPath;
260
+ }
261
+ if (!config.command) {
262
+ return null;
263
+ }
264
+ const appMatch = config.command.match(/^(.+\.app)\//i);
265
+ return appMatch ? appMatch[1] : null;
266
+ }
267
+
253
268
  /**
254
269
  * Get the server command and args, or return null if not found
255
270
  *
@@ -9,6 +9,7 @@
9
9
 
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
+ import { execFile } from "node:child_process";
12
13
 
13
14
  import type {
14
15
  ExtensionAPI,
@@ -32,7 +33,7 @@ import type {
32
33
  AutoSelectionEntryRangeData,
33
34
  } from "./types.js";
34
35
  import { AUTO_SELECTION_ENTRY_TYPE } from "./types.js";
35
- import { loadConfig, getServerCommand } from "./config.js";
36
+ import { loadConfig, getServerCommand, inferAppPath } from "./config.js";
36
37
  import { getRpClient, resetRpClient } from "./client.js";
37
38
  import {
38
39
  getBinding,
@@ -557,6 +558,8 @@ const RpToolSchema = Type.Object({
557
558
  export default function repopromptMcp(pi: ExtensionAPI) {
558
559
  let config: RpConfig = loadConfig();
559
560
  let initPromise: Promise<void> | null = null;
561
+ let shutdownRequested = false;
562
+ let extensionPaused = false;
560
563
 
561
564
  pi.on("before_agent_start", async () => {
562
565
  // Reload config so display knobs (collapsedMaxLines etc.) apply without requiring /reload
@@ -1225,6 +1228,9 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1225
1228
  // ───────────────────────────────────────────────────────────────────────────
1226
1229
 
1227
1230
  pi.on("session_start", async (_event, ctx) => {
1231
+ shutdownRequested = false;
1232
+ extensionPaused = false;
1233
+
1228
1234
  if (ctx.hasUI) {
1229
1235
  // This extension used to set a status bar item; clear it to avoid persisting stale UI state
1230
1236
  ctx.ui.setStatus("rp", undefined);
@@ -1244,16 +1250,45 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1244
1250
  }
1245
1251
 
1246
1252
  // Non-blocking initialization
1247
- initPromise = initializeExtension(pi, ctx, config);
1253
+ const pendingInit = initializeExtension(pi, ctx, config);
1254
+ initPromise = pendingInit;
1248
1255
 
1249
- initPromise.then(async () => {
1250
- initPromise = null;
1256
+ pendingInit.then(async () => {
1257
+ if (initPromise === pendingInit) {
1258
+ initPromise = null;
1259
+ }
1260
+ if (shutdownRequested) {
1261
+ return;
1262
+ }
1251
1263
  await syncAutoSelectionToCurrentBranch(ctx);
1252
- }).catch((err) => {
1253
- console.error("RepoPrompt MCP initialization failed:", err);
1254
- initPromise = null;
1264
+ }).catch(async (err) => {
1265
+ if (initPromise === pendingInit) {
1266
+ initPromise = null;
1267
+ }
1268
+ if (shutdownRequested) {
1269
+ return;
1270
+ }
1271
+ // If autoLaunchApp is enabled, try opening the app and retrying once
1272
+ if (config.autoLaunchApp) {
1273
+ const appPath = inferAppPath(config);
1274
+ if (appPath) {
1275
+ const launched = await tryLaunchApp(appPath);
1276
+ if (launched && !shutdownRequested) {
1277
+ try {
1278
+ await resetRpClient();
1279
+ await initializeExtension(pi, ctx, config);
1280
+ await syncAutoSelectionToCurrentBranch(ctx);
1281
+ return;
1282
+ } catch {
1283
+ // Fall through to pause
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+
1289
+ extensionPaused = true;
1255
1290
  if (ctx.hasUI) {
1256
- ctx.ui.notify(`RepoPrompt: ${err.message}`, "error");
1291
+ ctx.ui.notify("RepoPrompt unavailable — extension paused. Use /rp reconnect when ready.", "warning");
1257
1292
  }
1258
1293
  });
1259
1294
  });
@@ -1263,14 +1298,10 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1263
1298
  });
1264
1299
 
1265
1300
  pi.on("session_shutdown", async () => {
1266
- if (initPromise) {
1267
- try {
1268
- await initPromise;
1269
- } catch {
1270
- // Ignore
1271
- }
1272
- }
1301
+ shutdownRequested = true;
1302
+ initPromise = null;
1273
1303
 
1304
+ // Never block Pi shutdown on an MCP startup handshake that may be stuck waiting on the app
1274
1305
  clearReadcacheCaches();
1275
1306
  activeAutoSelectionState = null;
1276
1307
  await resetRpClient();
@@ -1340,15 +1371,15 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1340
1371
  const parts = args?.trim().split(/\s+/) ?? [];
1341
1372
  const subcommand = parts[0]?.toLowerCase() ?? "status";
1342
1373
 
1343
- // Allow status/readcache-status/readcache-refresh/reconnect while disconnected
1344
- if (
1345
- subcommand !== "reconnect" &&
1346
- subcommand !== "status" &&
1347
- subcommand !== "readcache-status" &&
1348
- subcommand !== "readcache_status" &&
1349
- subcommand !== "readcache-refresh" &&
1350
- subcommand !== "readcache_refresh"
1351
- ) {
1374
+ // Allow status/reconnect while disconnected or paused
1375
+ const alwaysAllowed = new Set(["reconnect", "status", "readcache-status", "readcache_status", "readcache-refresh", "readcache_refresh"]);
1376
+
1377
+ if (extensionPaused && !alwaysAllowed.has(subcommand)) {
1378
+ ctx.ui.notify("RepoPrompt extension is paused. Use /rp reconnect to resume.", "warning");
1379
+ return;
1380
+ }
1381
+
1382
+ if (!alwaysAllowed.has(subcommand)) {
1352
1383
  await ensureConnected(ctx);
1353
1384
  }
1354
1385
 
@@ -1587,16 +1618,31 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1587
1618
  break;
1588
1619
  }
1589
1620
 
1590
- case "reconnect":
1621
+ case "reconnect": {
1622
+ const wasPaused = extensionPaused;
1591
1623
  try {
1592
1624
  await resetRpClient();
1625
+ extensionPaused = false;
1593
1626
  await initializeExtension(pi, ctx, config);
1594
1627
  await syncAutoSelectionToCurrentBranch(ctx);
1595
1628
  ctx.ui.notify("RepoPrompt reconnected", "info");
1629
+
1630
+ if (wasPaused) {
1631
+ pi.sendMessage(
1632
+ {
1633
+ customType: "rp-availability",
1634
+ content: "RepoPrompt (`rp` tool) is now available.",
1635
+ display: false,
1636
+ },
1637
+ { triggerTurn: false },
1638
+ );
1639
+ }
1596
1640
  } catch (err) {
1641
+ extensionPaused = true;
1597
1642
  ctx.ui.notify(`Reconnection failed: ${err instanceof Error ? err.message : err}`, "error");
1598
1643
  }
1599
1644
  break;
1645
+ }
1600
1646
 
1601
1647
  default:
1602
1648
  ctx.ui.notify(
@@ -1640,6 +1686,13 @@ Mode priority: call > describe > search > windows > bind > status`,
1640
1686
  parameters: RpToolSchema,
1641
1687
 
1642
1688
  async execute(_toolCallId, params: RpToolParams, _signal, onUpdate, _ctx) {
1689
+ if (extensionPaused) {
1690
+ throw new Error(
1691
+ "The rp tool is not currently available due to a connection issue. " +
1692
+ "The user can run /rp reconnect when the RepoPrompt app is running."
1693
+ );
1694
+ }
1695
+
1643
1696
  // Provide a no-op if onUpdate is undefined
1644
1697
  const safeOnUpdate = onUpdate ?? (() => {});
1645
1698
 
@@ -2176,6 +2229,9 @@ Mode priority: call > describe > search > windows > bind > status`,
2176
2229
 
2177
2230
  let msg = `RepoPrompt Status\n`;
2178
2231
  msg += `─────────────────\n`;
2232
+ if (extensionPaused) {
2233
+ msg += `Extension: ⏸ paused (use /rp reconnect to resume)\n`;
2234
+ }
2179
2235
  msg += `Connection: ${client.isConnected ? "✓ connected" : "✗ disconnected"}\n`;
2180
2236
  msg += `Tools: ${client.tools.length}\n`;
2181
2237
 
@@ -2919,6 +2975,26 @@ async function promptForWindowSelection(
2919
2975
  );
2920
2976
  }
2921
2977
 
2978
+ /**
2979
+ * Try to launch the RepoPrompt app via `open`. Returns true if the app was launched
2980
+ * and appears to have started (the MCP server binary exists inside the bundle).
2981
+ */
2982
+ async function tryLaunchApp(appPath: string): Promise<boolean> {
2983
+ if (process.platform !== "darwin") {
2984
+ return false;
2985
+ }
2986
+ try {
2987
+ await new Promise<void>((resolve, reject) => {
2988
+ execFile("open", ["-a", appPath], (err) => (err ? reject(err) : resolve()));
2989
+ });
2990
+ // Give the app time to start its MCP server
2991
+ await new Promise((resolve) => setTimeout(resolve, 4000));
2992
+ return true;
2993
+ } catch {
2994
+ return false;
2995
+ }
2996
+ }
2997
+
2922
2998
  async function initializeExtension(
2923
2999
  pi: ExtensionAPI,
2924
3000
  ctx: ExtensionContext,
@@ -124,8 +124,12 @@ export interface RpConfig {
124
124
  // (tracks read slices/full files so chat_send/"Oracle" has context without manual selection)
125
125
  autoSelectReadSlices?: boolean; // When true, read_file calls add slices/full selection (default: true)
126
126
 
127
- // /rp oracle behavior
128
- oracleDefaultMode?: "chat" | "plan" | "edit" | "review"; // Default mode when /rp oracle omits --mode (default: "chat")
127
+ // App launch
128
+ autoLaunchApp?: boolean; // Auto-launch RepoPrompt.app on connection failure (default: false)
129
+ appPath?: string; // Explicit path to RepoPrompt.app (inferred from command if omitted)
130
+
131
+ // /rp oracle behavior
132
+ oracleDefaultMode?: "chat" | "plan" | "edit" | "review"; // Default mode when /rp oracle omits --mode (default: "chat")
129
133
  }
130
134
 
131
135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repoprompt-mcp",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "A token-efficient RepoPrompt integration for Pi with automated and branch-safe workspace management",
5
5
  "keywords": ["pi-package", "pi", "pi-coding-agent", "repoprompt", "mcp"],
6
6
  "license": "MIT",