pi-repoprompt-mcp 0.5.5 → 0.6.1

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.
@@ -754,7 +754,7 @@ function parseTabFromJson(raw: unknown): RpTab | null {
754
754
  }
755
755
 
756
756
  const obj = raw as Record<string, unknown>;
757
- const idRaw = obj.id ?? obj.tabId ?? obj.tab_id ?? obj.uuid;
757
+ const idRaw = obj.contextId ?? obj.context_id ?? obj.id ?? obj.tabId ?? obj.tab_id ?? obj.uuid;
758
758
  if (typeof idRaw !== "string" || !idRaw.trim()) {
759
759
  return null;
760
760
  }
@@ -786,7 +786,20 @@ function collectTabsFromJson(raw: unknown): RpTab[] {
786
786
  }
787
787
 
788
788
  const obj = raw as Record<string, unknown>;
789
- const containers = [obj.tabs, obj.tab, obj.createdTab, obj.created_tab, obj.selectedTab, obj.selected_tab];
789
+ const containers = [
790
+ obj.tabs,
791
+ obj.contexts,
792
+ obj.tab,
793
+ obj.context,
794
+ obj.createdTab,
795
+ obj.created_tab,
796
+ obj.createdContext,
797
+ obj.created_context,
798
+ obj.selectedTab,
799
+ obj.selected_tab,
800
+ obj.selectedContext,
801
+ obj.selected_context,
802
+ ];
790
803
 
791
804
  for (const candidate of containers) {
792
805
  const parsed = collectTabsFromJson(candidate);
@@ -835,7 +848,26 @@ function dedupeTabs(tabs: RpTab[]): RpTab[] {
835
848
 
836
849
  function parseTabLine(line: string): RpTab | null {
837
850
  const trimmed = line.trim();
838
- if (!trimmed || !trimmed.includes("`")) {
851
+ if (!trimmed) {
852
+ return null;
853
+ }
854
+
855
+ const contextMatch = trimmed.match(/^[\u2022-]\s*(.+?)(?:\s+\[([^\]]+)\])?\s+—\s+context_id:\s*`([^`]+)`/i);
856
+ if (contextMatch) {
857
+ const state = contextMatch[2] ?? "";
858
+ return {
859
+ id: contextMatch[3].trim(),
860
+ name: stripTrailingTabStateAnnotations(contextMatch[1].trim()) || contextMatch[3].trim(),
861
+ isActive: /\bactive\b|\bin-focus\b/i.test(state)
862
+ ? true
863
+ : /\bout-of-focus\b/i.test(state)
864
+ ? false
865
+ : undefined,
866
+ isBound: /\bbound\b/i.test(state) ? true : undefined,
867
+ };
868
+ }
869
+
870
+ if (!trimmed.includes("`")) {
839
871
  return null;
840
872
  }
841
873
 
@@ -869,6 +901,17 @@ function parseTabLine(line: string): RpTab | null {
869
901
  };
870
902
  }
871
903
 
904
+ function parseTabsFromBindContextText(text: string): RpTab[] {
905
+ return dedupeTabs(
906
+ text
907
+ .split("\n")
908
+ .map((line) => line.trim())
909
+ .filter((line) => /—\s+context_id:\s*`[^`]+`/i.test(line))
910
+ .map(parseTabLine)
911
+ .filter((tab): tab is RpTab => tab !== null)
912
+ );
913
+ }
914
+
872
915
  export function parseTabList(text: string): RpTab[] {
873
916
  const tabs: RpTab[] = [];
874
917
  let lastTab: RpTab | null = null;
@@ -899,132 +942,129 @@ function parseTabsFromJson(value: unknown): RpTab[] | null {
899
942
  return tabs.length > 0 ? dedupeTabs(tabs) : null;
900
943
  }
901
944
 
902
- function parseChatCountFromJson(value: unknown): number | undefined {
903
- if (!value) {
904
- return undefined;
905
- }
906
-
907
- if (Array.isArray(value)) {
908
- return value.length;
945
+ function findLiveTab(tabs: RpTab[], reference: string | undefined): RpTab | null {
946
+ if (!reference) {
947
+ return null;
909
948
  }
910
949
 
911
- if (typeof value !== "object") {
912
- return undefined;
913
- }
950
+ return tabs.find((tab) => tab.id === reference || tab.name === reference) ?? null;
951
+ }
914
952
 
915
- const obj = value as Record<string, unknown>;
916
- const directCount = parseCountMaybe(
917
- obj.count ?? obj.chatCount ?? obj.chat_count ?? obj.total ?? obj.totalCount ?? obj.total_count
918
- );
919
- if (directCount !== undefined) {
920
- return directCount;
921
- }
953
+ function isExplicitlyEmptyTab(tab: RpTab): boolean {
954
+ return tab.selectedFileCount === 0;
955
+ }
922
956
 
923
- for (const key of ["chats", "sessions", "items", "results"]) {
924
- const candidate = obj[key];
925
- if (Array.isArray(candidate)) {
926
- return candidate.length;
927
- }
928
- }
957
+ function orderReusableTabCandidates(tabs: RpTab[]): RpTab[] {
958
+ const ordered = [
959
+ ...tabs.filter((tab) => tab.isBound === true),
960
+ ...tabs.filter((tab) => tab.isBound !== true && tab.isActive === true),
961
+ ...tabs.filter((tab) => tab.isBound !== true && tab.isActive !== true),
962
+ ];
929
963
 
930
- return undefined;
964
+ return ordered.filter((tab, index) => ordered.findIndex((candidate) => candidate.id === tab.id) === index);
931
965
  }
932
966
 
933
- function parseChatCountFromText(text: string): number | undefined {
934
- const countMatch = text.match(/\bCount\b[^\d]*([\d,]+)/i);
935
- if (countMatch?.[1]) {
936
- return parseCountMaybe(countMatch[1]);
937
- }
967
+ function parseOracleSessionTabPrefixes(text: string): Set<string> {
968
+ const prefixes = new Set<string>();
938
969
 
939
- if (/\bNo chats\b/i.test(text)) {
940
- return 0;
970
+ for (const match of text.matchAll(/\btab=([A-F0-9-]{6,})(?:…|\b)/gi)) {
971
+ const prefix = match[1]?.trim().toUpperCase();
972
+ if (prefix) {
973
+ prefixes.add(prefix);
974
+ }
941
975
  }
942
976
 
943
- const sessionCount = text
944
- .split("\n")
945
- .map((line) => line.trim())
946
- .filter((line) => /^•\s*\[[^\]]+\]/.test(line)).length;
947
-
948
- return sessionCount > 0 ? sessionCount : undefined;
977
+ return prefixes;
949
978
  }
950
979
 
951
- async function fetchTabChatCount(
952
- tabId: string,
980
+ async function fetchOracleSessionTabPrefixes(
981
+ windowId: number,
953
982
  client: ReturnType<typeof getRpClient> = getRpClient()
954
- ): Promise<number | undefined> {
955
- if (!client.isConnected) {
956
- return undefined;
957
- }
958
-
959
- const chatsToolName = resolveToolName(client.tools, "chats");
960
- if (!chatsToolName) {
961
- return undefined;
983
+ ): Promise<Set<string> | null> {
984
+ const oracleUtilsToolName = resolveToolName(client.tools, "oracle_utils");
985
+ if (!oracleUtilsToolName) {
986
+ return null;
962
987
  }
963
988
 
964
- const result = await client.callTool(chatsToolName, {
965
- action: "list",
966
- scope: "tab",
967
- tab_id: tabId,
968
- limit: 1,
989
+ const result = await client.callTool(oracleUtilsToolName, {
990
+ op: "sessions",
991
+ limit: 200,
992
+ _windowID: windowId,
969
993
  });
970
994
 
971
995
  if (result.isError) {
972
- return undefined;
973
- }
974
-
975
- const countFromJson = parseChatCountFromJson(extractJsonContent(result.content));
976
- if (countFromJson !== undefined) {
977
- return countFromJson;
996
+ return null;
978
997
  }
979
998
 
980
- return parseChatCountFromText(extractTextContent(result.content));
999
+ return parseOracleSessionTabPrefixes(extractTextContent(result.content));
981
1000
  }
982
1001
 
983
- function findLiveTab(tabs: RpTab[], reference: string | undefined): RpTab | null {
984
- if (!reference) {
985
- return null;
1002
+ function tabHasOracleHistory(tabId: string, sessionTabPrefixes: Set<string> | null): boolean {
1003
+ if (!sessionTabPrefixes || sessionTabPrefixes.size === 0) {
1004
+ return false;
986
1005
  }
987
1006
 
988
- return tabs.find((tab) => tab.id === reference || tab.name === reference) ?? null;
989
- }
1007
+ const normalizedTabId = tabId.trim().toUpperCase();
1008
+ for (const prefix of sessionTabPrefixes) {
1009
+ if (normalizedTabId.startsWith(prefix)) {
1010
+ return true;
1011
+ }
1012
+ }
990
1013
 
991
- function isExplicitlyEmptyTab(tab: RpTab): boolean {
992
- return tab.selectedFileCount === 0;
1014
+ return false;
993
1015
  }
994
1016
 
995
- async function isSafeReusableTab(
1017
+ async function hasEmptySelection(
1018
+ windowId: number,
996
1019
  tab: RpTab,
997
1020
  client: ReturnType<typeof getRpClient> = getRpClient()
998
1021
  ): Promise<boolean> {
999
- if (!isExplicitlyEmptyTab(tab)) {
1022
+ if (isExplicitlyEmptyTab(tab)) {
1023
+ return true;
1024
+ }
1025
+
1026
+ const manageSelectionToolName = resolveToolName(client.tools, "manage_selection");
1027
+ if (!manageSelectionToolName) {
1000
1028
  return false;
1001
1029
  }
1002
1030
 
1003
- const chatCount = await fetchTabChatCount(tab.id, client);
1004
- return chatCount === 0;
1005
- }
1031
+ const result = await client.callTool(manageSelectionToolName, {
1032
+ op: "get",
1033
+ view: "summary",
1034
+ _windowID: windowId,
1035
+ context_id: tab.id,
1036
+ });
1006
1037
 
1007
- function orderReusableEmptyTabs(tabs: RpTab[]): RpTab[] {
1008
- const emptyTabs = tabs.filter(isExplicitlyEmptyTab);
1009
- if (emptyTabs.length === 0) {
1010
- return [];
1038
+ if (result.isError) {
1039
+ return false;
1011
1040
  }
1012
1041
 
1013
- const ordered = [
1014
- ...emptyTabs.filter((tab) => tab.isBound === true),
1015
- ...emptyTabs.filter((tab) => tab.isBound !== true && tab.isActive === true),
1016
- ...emptyTabs.filter((tab) => tab.isBound !== true && tab.isActive !== true),
1017
- ];
1042
+ const text = extractTextContent(result.content);
1043
+ return /\b0 total tokens\b/i.test(text);
1044
+ }
1018
1045
 
1019
- return ordered.filter((tab, index) => ordered.findIndex((candidate) => candidate.id === tab.id) === index);
1046
+ async function isSafeReusableTab(
1047
+ windowId: number,
1048
+ tab: RpTab,
1049
+ sessionTabPrefixes: Set<string> | null,
1050
+ client: ReturnType<typeof getRpClient> = getRpClient()
1051
+ ): Promise<boolean> {
1052
+ if (tabHasOracleHistory(tab.id, sessionTabPrefixes)) {
1053
+ return false;
1054
+ }
1055
+
1056
+ return await hasEmptySelection(windowId, tab, client);
1020
1057
  }
1021
1058
 
1022
1059
  async function findReusableSafeTab(
1060
+ windowId: number,
1023
1061
  tabs: RpTab[],
1024
1062
  client: ReturnType<typeof getRpClient> = getRpClient()
1025
1063
  ): Promise<RpTab | null> {
1026
- for (const tab of orderReusableEmptyTabs(tabs)) {
1027
- if (await isSafeReusableTab(tab, client)) {
1064
+ const sessionTabPrefixes = await fetchOracleSessionTabPrefixes(windowId, client);
1065
+
1066
+ for (const tab of orderReusableTabCandidates(tabs)) {
1067
+ if (await isSafeReusableTab(windowId, tab, sessionTabPrefixes, client)) {
1028
1068
  return tab;
1029
1069
  }
1030
1070
  }
@@ -1066,14 +1106,14 @@ export async function fetchWindowTabs(
1066
1106
  throw new Error("Not connected to RepoPrompt");
1067
1107
  }
1068
1108
 
1069
- const manageWorkspacesToolName = resolveToolName(client.tools, "manage_workspaces");
1070
- if (!manageWorkspacesToolName) {
1109
+ const bindContextToolName = resolveToolName(client.tools, "bind_context");
1110
+ if (!bindContextToolName) {
1071
1111
  return [];
1072
1112
  }
1073
1113
 
1074
- const result = await client.callTool(manageWorkspacesToolName, {
1075
- action: "list_tabs",
1076
- ...bindingWindowArgs(windowId),
1114
+ const result = await client.callTool(bindContextToolName, {
1115
+ op: "list",
1116
+ window_id: windowId,
1077
1117
  });
1078
1118
 
1079
1119
  if (result.isError) {
@@ -1086,7 +1126,7 @@ export async function fetchWindowTabs(
1086
1126
  return tabsFromJson;
1087
1127
  }
1088
1128
 
1089
- return parseTabList(extractTextContent(result.content));
1129
+ return parseTabsFromBindContextText(extractTextContent(result.content));
1090
1130
  }
1091
1131
 
1092
1132
  async function selectTab(
@@ -1094,16 +1134,15 @@ async function selectTab(
1094
1134
  tabId: string,
1095
1135
  client: ReturnType<typeof getRpClient> = getRpClient()
1096
1136
  ): Promise<void> {
1097
- const manageWorkspacesToolName = resolveToolName(client.tools, "manage_workspaces");
1098
- if (!manageWorkspacesToolName) {
1137
+ const bindContextToolName = resolveToolName(client.tools, "bind_context");
1138
+ if (!bindContextToolName) {
1099
1139
  return;
1100
1140
  }
1101
1141
 
1102
- const result = await client.callTool(manageWorkspacesToolName, {
1103
- action: "select_tab",
1104
- tab: tabId,
1105
- focus: false,
1106
- ...bindingWindowArgs(windowId),
1142
+ const result = await client.callTool(bindContextToolName, {
1143
+ op: "bind",
1144
+ window_id: windowId,
1145
+ context_id: tabId,
1107
1146
  });
1108
1147
 
1109
1148
  if (result.isError) {
@@ -1290,7 +1329,7 @@ export async function ensureBindingHasTab(
1290
1329
  }
1291
1330
 
1292
1331
  if (!binding.tab || reuseSoleEmptyTab || recoverIfMissing) {
1293
- const reusableSafeTab = await findReusableSafeTab(liveTabs, client);
1332
+ const reusableSafeTab = await findReusableSafeTab(binding.windowId, liveTabs, client);
1294
1333
  if (reusableSafeTab) {
1295
1334
  return await adoptTab(reusableSafeTab, true);
1296
1335
  }
@@ -1498,7 +1537,7 @@ export function getBindingArgs(): Record<string, unknown> {
1498
1537
  };
1499
1538
 
1500
1539
  if (currentBinding.tab) {
1501
- args._tabID = currentBinding.tab;
1540
+ args.context_id = currentBinding.tab;
1502
1541
  }
1503
1542
 
1504
1543
  return args;
@@ -813,7 +813,7 @@ export default function repopromptMcp(pi: ExtensionAPI) {
813
813
  function bindingArgsForAutoSelectionState(state: AutoSelectionEntryData): Record<string, unknown> {
814
814
  return {
815
815
  _windowID: state.windowId,
816
- ...(state.tab ? { _tabID: state.tab } : {}),
816
+ ...(state.tab ? { context_id: state.tab } : {}),
817
817
  };
818
818
  }
819
819
 
@@ -1582,9 +1582,9 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1582
1582
  try {
1583
1583
  await ensureTabScopedBinding(ctx, "RepoPrompt binding has no tab. Use /rp bind or /rp tab new first.");
1584
1584
 
1585
- const chatSendToolName = resolveToolName(client.tools, "chat_send");
1586
- if (!chatSendToolName) {
1587
- ctx.ui.notify("RepoPrompt tool 'chat_send' not available", "error");
1585
+ const oracleSendToolName = resolveToolName(client.tools, "oracle_send");
1586
+ if (!oracleSendToolName) {
1587
+ ctx.ui.notify("RepoPrompt tool 'oracle_send' not available", "error");
1588
1588
  return;
1589
1589
  }
1590
1590
 
@@ -1598,7 +1598,7 @@ export default function repopromptMcp(pi: ExtensionAPI) {
1598
1598
  if (chatName) callArgs.chat_name = chatName;
1599
1599
  if (chatId) callArgs.chat_id = chatId;
1600
1600
 
1601
- const result = await client.callTool(chatSendToolName, callArgs);
1601
+ const result = await client.callTool(oracleSendToolName, callArgs);
1602
1602
 
1603
1603
  const text = extractTextContent(result.content);
1604
1604
 
@@ -2541,7 +2541,15 @@ Mode priority: call > describe > search > windows > bind > status`,
2541
2541
  const userArgs = (params.args ?? {}) as Record<string, unknown>;
2542
2542
  const normalizedTool = normalizeToolName(tool.name);
2543
2543
 
2544
- if (getBinding() && !getBinding()?.tab && normalizedTool !== "manage_workspaces" && normalizedTool !== "list_windows") {
2544
+ if (
2545
+ getBinding() &&
2546
+ !getBinding()?.tab &&
2547
+ normalizedTool !== "manage_workspaces" &&
2548
+ normalizedTool !== "list_windows" &&
2549
+ normalizedTool !== "bind_context" &&
2550
+ normalizedTool !== "agent_run" &&
2551
+ normalizedTool !== "agent_manage"
2552
+ ) {
2545
2553
  if (!ctx) {
2546
2554
  return {
2547
2555
  content: [{ type: "text" as const, text: "RepoPrompt binding has no tab. Re-bind with /rp bind before calling tab-scoped tools." }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repoprompt-mcp",
3
- "version": "0.5.5",
3
+ "version": "0.6.1",
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",