pi-repoprompt-mcp 0.3.0 → 0.4.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/README.md CHANGED
@@ -1,15 +1,10 @@
1
1
  # RepoPrompt MCP for Pi (`pi-repoprompt-mcp`)
2
2
 
3
- A token-efficient RepoPrompt MCP integration for Pi.
3
+ This package provides a single tool (`rp`) that exposes the RepoPrompt MCP tools to Pi, includes branch-safe window and tab binding (automatically detecting the right window by `cwd`, provisioning a safe tab, persisting both across sessions and session tree nodes, and letting you pick windows and tabs interactively) and batches of read files (automatically selected as context in the RepoPrompt desktop app), renders RepoPrompt tool outputs (syntax + diff highlighting), and applies guardrails for destructive operations.
4
4
 
5
- Exposes a single `rp` tool (RepoPrompt MCP proxy) plus `/rp …` commands, with:
6
- - window/tab binding (auto-detect by `cwd`, optional persistence); these are branch-safe across navigation of the session DAG via `/tree` and across `/fork`ed sessions
7
- - output rendering (syntax + diff highlighting; uses `delta` when installed, honoring the user's global git/delta color config, with graceful fallback)
8
- - safety guardrails (deletes blocked unless explicitly allowed; optional edit confirmation)
9
- - optional [Gurpartap/pi-readcache](https://github.com/Gurpartap/pi-readcache)-like caching for RepoPrompt `read_file` results (unchanged markers + diffs) to save on tokens
10
- - optional auto-selection in the RP UI (e.g. for use in RP Chat) of slices/files the agent has read; these selections are also branch-safe across `/tree` navigation and `/fork`ed session
5
+ The package's window- and tab-related management features allow a workflow where new Pi sessions automatically attach to the required workspace and tab without clobbering your, or other agents', parallel usage of RepoPrompt. Because it recovers the window, tab, and auto-selected read-files context when you rewind via `/tree` or restore a session, all the context the agent has built up (and automatically selected in the RepoPrompt app) by reading files and slices up to that point always remains available in the app for RP Chat (see `/rp oracle` below) or external "oracle" (e.g. GPT-x Pro) use cases. **Note: this recovery currently requires the original workspace (but not necessarily its original tabs) to be open, not just any workspace containing the same required root(s).**
11
6
 
12
- ## Install
7
+ ## Installation
13
8
 
14
9
  From npm:
15
10
 
@@ -35,28 +30,104 @@ Add to `~/.pi/agent/settings.json` (or replace an existing unfiltered `git:githu
35
30
  }
36
31
  ```
37
32
 
33
+ ## Features
34
+
35
+ ### Window and tab binding
36
+
37
+ - Auto-binds to the RepoPrompt window that matches `process.cwd()` (by workspace roots, resolving symlinks to their real paths before matching)
38
+ - If multiple windows match, you're prompted to pick one
39
+ - Window binding is (optionally) persisted across session reloads and session tree nodes
40
+ - If a bound window has a completely blank tab, the package binds to that tab; if the tab is dirty, then it provisions a new tab and binds to that
41
+ - Deterministically reconciles the session tree node's bound tab, and can restore the tab already associated with that node or provision a new safe tab when needed
42
+ - User-driven binding via `/rp bind` (windows) or `/rp tab` (tabs); agents can use `rp({ bind: ... })`
43
+ - In addition to window bindings, tab bindings and auto-selected read-files context is stored and automatically recovered across node rewinds via `/tree`, different sessions (e.g., created via `/fork`), and resumed sessions
44
+
45
+ Forked sessions inherit the parent session-plus-node's window, tab, and auto-selected context snapshot at the fork point (unless you rewind in the forked session and switch window/tab/etc.), then can diverge independently as later reads or manual tab switches are performed in the child session. Binding is non-invasive, in that it doesn't change RepoPrompt's globally active window, and automatic tab provisioning uses background tabs (`focus=false`) without stealing UI focus. This is to prevent interference when multiple agents (or your manual usage of RepoPrompt in parallel to a Pi session) are using this package and need to target different windows or tabs simultaneously.
46
+
47
+ ### Output rendering
48
+
49
+ - Syntax highlighting for read files' code blocks and for codemaps
50
+ - Diff highlighting for diff blocks (`delta` when installed, honoring the user's global git/delta color config, graceful fallback otherwise)
51
+ - Markdown-aware styling for headings and lists
52
+ - Collapsed output by default (expand using Pi's standard UI controls)
53
+
54
+ ### Safety checks
55
+
56
+ - Delete operations are blocked unless you pass `allowDelete: true`
57
+ - Optional edit confirmation gate for edit-like operations (`confirmEdits`)
58
+ - Warn on in-place workspace switches (when applicable)
59
+
38
60
  ## Requirements
39
61
 
40
- - RepoPrompt installed
41
- - RepoPrompt MCP server reachable (stdio transport)
62
+ - RepoPrompt MCP server configured and reachable (stdio transport)
42
63
  - If the server is not configured/auto-detected, the package will still load, but `rp(...)` will error until you configure it
43
- - `rp-cli` on `PATH` is recommended (used as a fallback for window discovery)
64
+ - `rp-cli` available in `PATH` is recommended (used as a fallback for window discovery)
65
+
66
+ ### Compatibility notes
67
+
68
+ This package tries to be tolerant of **tool name prefixing** (e.g. `RepoPrompt_list_windows` vs `list_windows`), but it is still dependent on a small set of capabilities and their semantics remaining reasonably stable across RepoPrompt versions:
69
+
70
+ - **Window discovery**: `list_windows`
71
+ - If `list_windows` is not exposed by the MCP server, the package falls back to `rp-cli -e 'windows'`
72
+ - If neither is available, window listing/binding features will be limited
73
+ - **Workspace root discovery (auto-bind by cwd)**: `get_file_tree` with `{ type: "roots" }` (scoped by `_windowID`)
74
+ - If unavailable (or if parameters/semantics change), auto-binding may be disabled or less accurate
75
+ - Selection summary: `manage_selection` with `{ op: "get", view: "files" }` and `{ op: "get", view: "summary" }`
76
+ - If these are unavailable (or if parameters/semantics change), the status output may omit file/token counts
77
+
78
+ If RepoPrompt renames/removes these tools or changes their required parameters/output formats, this package may need updates
44
79
 
45
80
  ## Usage
46
81
 
47
- Commands:
48
- - `/rp status`
49
- - `/rp windows`
50
- - `/rp bind`
51
- - `/rp reconnect`
52
- - `/rp readcache-status`
53
- - `/rp readcache-refresh <path> [start-end]`
82
+ ### Commands
83
+
84
+ - `/rp status` — show status (connection + binding), including the currently bound tab name and a label like `[bound, in-focus]` or `[bound, out-of-focus]`, plus current selected file counts and estimated token counts
85
+ - `/rp windows` — list available RepoPrompt windows
86
+ - `/rp bind` — interactive workflow for choosing the RepoPrompt window
87
+ - `/rp bind <id> [tab]` — direct option if you already know the target window id (and optionally an exact tab name or tab id); when `[tab]` is omitted, the package restores the branch's tab for that window or provisions a fresh background tab once
88
+ - `/rp tab` interactive tab picker for the current bound window, with `Create new tab` as the first option followed by existing tab names
89
+ - `/rp tab new` — create and bind a fresh tab on the current bound window
90
+ - `/rp tab <name-or-id>` — bind an existing tab on the current bound window by name or id
91
+ - `/rp oracle [--mode <chat|plan|edit|review>] [--name <chat name>] [--continue|--chat-id <id>] <message>` — ask RepoPrompt chat with current selection context. If `--mode` not specified, uses `oracleDefaultMode` config.
92
+ - `/rp reconnect` — reconnect to RepoPrompt
93
+
94
+ ### Tool: `rp`
95
+
96
+ Examples:
54
97
 
55
- Tool:
56
98
  ```ts
99
+ // Status (connection + binding)
100
+ rp({ })
101
+
102
+ // List windows (best-effort; uses MCP tool if available, otherwise rp-cli)
57
103
  rp({ windows: true })
104
+
105
+ // Bind to a specific window (does not change RepoPrompt active window)
58
106
  rp({ bind: { window: 3 } })
107
+
108
+ // Bind to an exact tab in that window
109
+ rp({ bind: { window: 3, tab: "T2" } })
110
+
111
+ // Search or describe tools
112
+ rp({ search: "file" })
113
+ rp({ describe: "apply_edits" })
114
+
115
+ // Call a RepoPrompt tool (binding args are injected automatically)
59
116
  rp({ call: "read_file", args: { path: "src/main.ts" } })
117
+
118
+ // Edit confirmation gate (only required if confirmEdits=true in config)
119
+ rp({
120
+ call: "apply_edits",
121
+ args: { path: "file.ts", search: "old", replace: "new" },
122
+ confirmEdits: true
123
+ })
124
+
125
+ // Delete guard override
126
+ rp({
127
+ call: "file_actions",
128
+ args: { action: "delete", path: "temp.txt" },
129
+ allowDelete: true
130
+ })
60
131
  ```
61
132
 
62
133
  ## Configuration
@@ -65,32 +136,78 @@ Create `~/.pi/agent/extensions/repoprompt-mcp.json`:
65
136
 
66
137
  ```json
67
138
  {
139
+ "command": "rp-mcp-server",
140
+ "args": [],
141
+
68
142
  "autoBindOnStart": true,
69
143
  "persistBinding": true,
144
+
70
145
  "confirmDeletes": true,
71
146
  "confirmEdits": false,
147
+
72
148
  "readcacheReadFile": true,
73
- "collapsedMaxLines": 3
149
+ "autoSelectReadSlices": true,
150
+ "oracleDefaultMode": "chat",
151
+
152
+ "collapsedMaxLines": 3,
153
+ "suppressHostDisconnectedLog": true
74
154
  }
75
155
  ```
76
156
 
77
- `collapsedMaxLines` controls how many lines of RepoPrompt tool output Pi shows in collapsed view before you expand the result. It applies across `rp(...)` calls, so it is the main knob for keeping window listings, reads, and other RepoPrompt responses compact in the TUI. `3` is the best default if you want maximally compressed but still informative output.
157
+ `collapsedMaxLines` controls how many lines of RepoPrompt tool output Pi shows before the result is expanded. This applies to the collapsed preview for all `rp(...)` calls, including commands like window listings and file reads. **Recommended setting for maximally compressed** but still informative output: `3`.
78
158
 
79
- If the MCP server is not auto-detected, set `command` explicitly:
159
+ Options:
80
160
 
81
- ```json
82
- {
83
- "command": "/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp",
84
- "args": []
85
- }
86
- ```
161
+ | Option | Default | Description |
162
+ |---|---:|---|
163
+ | `command` | auto-detect | MCP server command |
164
+ | `args` | `[]` | MCP server args |
165
+ | `env` | unset | Extra environment variables for the MCP server |
166
+ | `autoBindOnStart` | `true` | Auto-detect and bind on session start, then reconcile the branch-safe tab for the chosen window |
167
+ | `persistBinding` | `true` | Persist window and tab bindings in Pi session history for branch-safe replay |
168
+ | `confirmDeletes` | `true` | Block delete operations unless `allowDelete: true` |
169
+ | `confirmEdits` | `false` | Block edit-like operations unless `confirmEdits: true` |
170
+ | `readcacheReadFile` | `false` | Enable [pi-readcache](https://github.com/Gurpartap/pi-readcache)-like caching for RepoPrompt `read_file` calls (returns unchanged markers/diffs on repeat reads to save on tokens and prevent context bloat) |
171
+ | `autoSelectReadSlices` | `true` | Automatically track `read_file` calls by adding slices/full-file selection via `manage_selection`, so `chat_send` (or a manually created chat in the RP app) uses everything the agent has read as context; these file/slice selections are **branch-safe** across `/tree` rewinds and `/fork`ed session branches via package-owned snapshot replay |
172
+ | `oracleDefaultMode` | `"chat"` | Default mode for `/rp oracle` when `--mode` is omitted (`chat`, `plan`, `edit`, or `review`) |
173
+ | `collapsedMaxLines` | `15` | Lines shown in collapsed view |
174
+ | `suppressHostDisconnectedLog` | `true` | Filter noisy stderr from macOS `repoprompt-mcp` (disconnect/retry bootstrap logs) |
87
175
 
88
- (Alternatively, configure RepoPrompt in `~/.pi/agent/mcp.json`)
176
+ Automatic tab restoration and provisioning is driven by `autoBindOnStart` and `persistBinding`; there is no separate tab-only configuration surface.
89
177
 
90
- For more detail, see: `extensions/repoprompt-mcp/README.md` in the dot314 repo.
178
+ Note: when `readcacheReadFile` is enabled, the package may persist UTF-8 file snapshots to an on-disk content-addressed store under
179
+ `<repo-root>/.pi/readcache/objects` to compute diffs/unchanged markers across calls. Common secret filenames (e.g. `.env*`, `*.pem`) are excluded,
180
+ but this is best-effort
91
181
 
92
182
  ## Readcache gotchas
93
183
 
94
184
  - `raw: true` disables readcache (and rendering). Don't use unless debugging
95
185
  - Need full content? use `bypass_cache: true` in `read_file` args
96
186
  - Multi-root: use absolute or specific relative paths (MCP `read_file` has no `RootName:` disambiguation)
187
+
188
+ ## Troubleshooting
189
+
190
+ ### "Not connected to RepoPrompt"
191
+ - Ensure RepoPrompt is running
192
+ - Verify the MCP server command in config
193
+ - Run `/rp reconnect`
194
+
195
+ ### Pi becomes unresponsive after closing/restarting RepoPrompt
196
+ 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 package will drop the connection and you can recover with `/rp reconnect`.
197
+
198
+ ### "No matching window found"
199
+ - Your `cwd` may not match any RepoPrompt workspace root
200
+ - Use `/rp windows` to list windows
201
+ - Use `/rp bind` to pick one
202
+
203
+ ### Window listing doesn't work
204
+ - If the MCP server does not expose a `list_windows` tool, this package uses `rp-cli -e 'windows'`
205
+ - Make sure `rp-cli` is installed and on your `PATH`
206
+ - If RepoPrompt is in single-window mode, `rp-cli -e 'windows'` may report single-window mode
207
+
208
+ ### Delete operation blocked
209
+ - Pass `allowDelete: true` on the `rp` call
210
+
211
+ ## License
212
+
213
+ MIT
@@ -3,6 +3,12 @@
3
3
  import * as fs from "node:fs";
4
4
  import { stat } from "node:fs/promises";
5
5
 
6
+ import type {
7
+ AutoSelectionEntryData,
8
+ AutoSelectionEntryRangeData,
9
+ AutoSelectionEntrySliceData,
10
+ } from "./types.js";
11
+
6
12
  export type SelectionMode = "full" | "slices" | "codemap_only";
7
13
 
8
14
  export interface SelectionStatus {
@@ -118,8 +124,128 @@ export function inferSelectionStatus(selectionText: string, selectionPath: strin
118
124
  continue;
119
125
  }
120
126
 
121
- const parentPrefix = prefixes[indentDepth] ?? baseDir;
122
- if (!parentPrefix) {
127
+ const parentPrefix = prefixes[indentDepth] ?? baseDir ?? (indentDepth === 0 ? "" : null);
128
+ if (parentPrefix === null) {
129
+ continue;
130
+ }
131
+
132
+ const nodePath = joinPath(parentPrefix, name);
133
+
134
+ if (name.endsWith("/")) {
135
+ prefixes[indentDepth + 1] = normalizeDir(nodePath);
136
+ prefixes.length = indentDepth + 2;
137
+ continue;
138
+ }
139
+
140
+ const match = considerMatch(nodePath, rest);
141
+ if (match) {
142
+ return match;
143
+ }
144
+ }
145
+
146
+ return null;
147
+ }
148
+
149
+ export function inferSelectionSliceRanges(
150
+ selectionText: string,
151
+ selectionPath: string
152
+ ): AutoSelectionEntryRangeData[] | null {
153
+ if (!selectionText || !selectionPath) {
154
+ return null;
155
+ }
156
+
157
+ const target = toPosixPath(selectionPath).replace(/\/+$/, "");
158
+
159
+ const normalizeDir = (dir: string): string => {
160
+ const normalized = toPosixPath(dir).trim();
161
+ return normalized.endsWith("/") ? normalized : `${normalized}/`;
162
+ };
163
+
164
+ const joinPath = (dir: string, name: string): string => {
165
+ if (!dir) {
166
+ return name;
167
+ }
168
+ return dir.endsWith("/") ? `${dir}${name}` : `${dir}/${name}`;
169
+ };
170
+
171
+ const lines = selectionText.split("\n");
172
+
173
+ let section: "selected" | "codemaps" | null = null;
174
+
175
+ let baseDir: string | null = null;
176
+ const prefixes: string[] = [];
177
+
178
+ const considerMatch = (nodePath: string, rest: string): AutoSelectionEntryRangeData[] | null => {
179
+ const normalizedNode = toPosixPath(nodePath).replace(/\/+$/, "");
180
+ if (normalizedNode !== target) {
181
+ return null;
182
+ }
183
+
184
+ if (section !== "selected") {
185
+ return null;
186
+ }
187
+
188
+ const allRangesMatch = rest.match(/\(lines\s+([^)]*)\)/i);
189
+ if (!allRangesMatch) {
190
+ return null;
191
+ }
192
+
193
+ const rangesText = allRangesMatch[1] ?? "";
194
+ const ranges = [...rangesText.matchAll(/(\d+)\s*[-–—]\s*(\d+)/g)]
195
+ .map((m) => ({
196
+ start_line: Number(m[1]),
197
+ end_line: Number(m[2]),
198
+ }))
199
+ .filter((range) => Number.isFinite(range.start_line) && Number.isFinite(range.end_line))
200
+ .filter((range) => range.start_line > 0 && range.end_line >= range.start_line);
201
+
202
+ return ranges.length > 0 ? ranges : null;
203
+ };
204
+
205
+ for (const rawLine of lines) {
206
+ const line = toPosixPath(rawLine).trimEnd();
207
+
208
+ if (line.includes("### Selected Files")) {
209
+ section = "selected";
210
+ baseDir = null;
211
+ prefixes.length = 0;
212
+ continue;
213
+ }
214
+
215
+ if (line.includes("### Codemaps")) {
216
+ section = "codemaps";
217
+ baseDir = null;
218
+ prefixes.length = 0;
219
+ continue;
220
+ }
221
+
222
+ if (line.endsWith("/") && !line.includes("──") && !line.includes(" — ")) {
223
+ baseDir = normalizeDir(line);
224
+ prefixes.length = 0;
225
+ prefixes.push(baseDir);
226
+ continue;
227
+ }
228
+
229
+ const markerMatch = line.match(/(├──|└──)\s+(.*)$/);
230
+ if (!markerMatch) {
231
+ continue;
232
+ }
233
+
234
+ const marker = markerMatch[1] ?? "";
235
+ const markerIdx = line.indexOf(marker);
236
+ const indentPrefix = markerIdx >= 0 ? line.slice(0, markerIdx) : "";
237
+
238
+ const indentDepth = Math.floor(indentPrefix.length / 4);
239
+
240
+ const rest = (markerMatch[2] ?? "").trim();
241
+ const name = rest.split(" — ")[0]?.trim() ?? "";
242
+
243
+ if (!name) {
244
+ continue;
245
+ }
246
+
247
+ const parentPrefix = prefixes[indentDepth] ?? baseDir ?? (indentDepth === 0 ? "" : null);
248
+ if (parentPrefix === null) {
123
249
  continue;
124
250
  }
125
251
 
@@ -203,6 +329,30 @@ export async function countFileLines(absolutePath: string): Promise<number> {
203
329
  return lines;
204
330
  }
205
331
 
332
+ export function isWholeFileReadFromArgs(
333
+ startLine: number | undefined,
334
+ limit: number | undefined,
335
+ totalLines: number | undefined
336
+ ): boolean {
337
+ if (typeof totalLines !== "number" || totalLines <= 0 || typeof startLine !== "number") {
338
+ return false;
339
+ }
340
+
341
+ if (startLine > 0) {
342
+ if (typeof limit !== "number" || limit <= 0) {
343
+ return false;
344
+ }
345
+
346
+ return startLine === 1 && totalLines <= limit;
347
+ }
348
+
349
+ if (startLine < 0) {
350
+ return Math.abs(startLine) >= totalLines;
351
+ }
352
+
353
+ return false;
354
+ }
355
+
206
356
  export function computeSliceRangeFromReadArgs(
207
357
  startLine: number | undefined,
208
358
  limit: number | undefined,
@@ -212,6 +362,10 @@ export function computeSliceRangeFromReadArgs(
212
362
  return null;
213
363
  }
214
364
 
365
+ if (isWholeFileReadFromArgs(startLine, limit, totalLines)) {
366
+ return null;
367
+ }
368
+
215
369
  // Positive range reads
216
370
  if (startLine > 0) {
217
371
  if (typeof limit !== "number" || limit <= 0) {
@@ -241,3 +395,80 @@ export function computeSliceRangeFromReadArgs(
241
395
 
242
396
  return null;
243
397
  }
398
+
399
+ function normalizeSelectionRanges(ranges: AutoSelectionEntryRangeData[]): AutoSelectionEntryRangeData[] {
400
+ const normalized = ranges
401
+ .map((range) => ({
402
+ start_line: Number(range.start_line),
403
+ end_line: Number(range.end_line),
404
+ }))
405
+ .filter((range) => Number.isFinite(range.start_line) && Number.isFinite(range.end_line))
406
+ .filter((range) => range.start_line > 0 && range.end_line >= range.start_line)
407
+ .sort((a, b) => {
408
+ if (a.start_line !== b.start_line) {
409
+ return a.start_line - b.start_line;
410
+ }
411
+ return a.end_line - b.end_line;
412
+ });
413
+
414
+ const merged: AutoSelectionEntryRangeData[] = [];
415
+ for (const range of normalized) {
416
+ const last = merged[merged.length - 1];
417
+ if (!last) {
418
+ merged.push(range);
419
+ continue;
420
+ }
421
+
422
+ if (range.start_line <= last.end_line + 1) {
423
+ last.end_line = Math.max(last.end_line, range.end_line);
424
+ continue;
425
+ }
426
+
427
+ merged.push(range);
428
+ }
429
+
430
+ return merged;
431
+ }
432
+
433
+ export function applyFullReadToSelectionState(
434
+ state: AutoSelectionEntryData,
435
+ selectionPath: string
436
+ ): AutoSelectionEntryData {
437
+ const normalizedPath = toPosixPath(selectionPath);
438
+
439
+ return {
440
+ ...state,
441
+ fullPaths: [...state.fullPaths, normalizedPath],
442
+ slicePaths: state.slicePaths.filter((entry) => entry.path !== normalizedPath),
443
+ };
444
+ }
445
+
446
+ export function applySliceReadToSelectionState(
447
+ state: AutoSelectionEntryData,
448
+ selectionPath: string,
449
+ range: AutoSelectionEntryRangeData
450
+ ): AutoSelectionEntryData {
451
+ const normalizedPath = toPosixPath(selectionPath);
452
+
453
+ if (state.fullPaths.includes(normalizedPath)) {
454
+ return {
455
+ ...state,
456
+ fullPaths: [...state.fullPaths],
457
+ slicePaths: [...state.slicePaths],
458
+ };
459
+ }
460
+
461
+ const existing = state.slicePaths.find((entry) => entry.path === normalizedPath);
462
+
463
+ const nextSlicePaths: AutoSelectionEntrySliceData[] = state.slicePaths.filter((entry) => entry.path !== normalizedPath);
464
+ nextSlicePaths.push({
465
+ path: normalizedPath,
466
+ ranges: normalizeSelectionRanges([...(existing?.ranges ?? []), range]),
467
+ });
468
+
469
+ return {
470
+ ...state,
471
+ fullPaths: [...state.fullPaths],
472
+ slicePaths: nextSlicePaths,
473
+ };
474
+ }