pi-codemcp 1.1.2 → 1.2.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.
@@ -45,7 +45,7 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
45
45
  return;
46
46
  }
47
47
 
48
- await showServerManagerModal(ctx, {
48
+ const managerResult = await showServerManagerModal(ctx, {
49
49
  servers,
50
50
  chains: chainStatesFromViews(savedChains),
51
51
  settings,
@@ -74,6 +74,7 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
74
74
  onDeleteChain: async (chain) =>
75
75
  chainStatesFromViews(await chains.delete(chain.name, chain.scope)),
76
76
  });
77
+ if (managerResult === "report-problem") await promptForProblemReport(pi, ctx);
77
78
  } catch (error) {
78
79
  ctx.ui.notify(summarizeError(error), "error");
79
80
  }
@@ -163,6 +164,23 @@ export async function saveManagerChanges(
163
164
 
164
165
  export default createCodeMcpExtension();
165
166
 
167
+ export async function promptForProblemReport(
168
+ pi: Pick<ExtensionAPI, "sendUserMessage">,
169
+ ctx: Pick<ExtensionCommandContext, "ui">,
170
+ ): Promise<void> {
171
+ const description = await ctx.ui.editor("What went wrong?", "");
172
+ if (description?.trim()) pi.sendUserMessage(formatProblemReportPrompt(description.trim()));
173
+ }
174
+
175
+ export function formatProblemReportPrompt(description: string): string {
176
+ return `Something went wrong with pi-codemcp.
177
+
178
+ User's description:
179
+ ${description}
180
+
181
+ Investigate the problem in current pi setup. Inspect the available pi-codemcp configuration, environment, and installed package as needed. Determine the likely cause, then prepare a GitHub issue for https://github.com/yolonir/pi-codemcp. Do not include any personal or sensitive information in the issue. Do not autosumbit issue without clear approval.`;
182
+ }
183
+
166
184
  function requireServerStatus(
167
185
  status: Record<string, unknown>,
168
186
  serverName: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codemcp",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Typed, sandboxed Code Mode access to configured MCP servers for Pi",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.10",
@@ -298,6 +298,7 @@ class MontyExecutor:
298
298
  type_stubs = catalog.type_stubs_for(referenced, include=spec.name)
299
299
  async with self._execution_lock:
300
300
  await self._type_check(wrapped, catalog, type_stubs)
301
+ await self._compile_runtime(code, catalog, has_input=True)
301
302
 
302
303
  def _new_context(
303
304
  self,
@@ -351,6 +352,9 @@ class MontyExecutor:
351
352
  except pydantic_monty.MontySyntaxError as error:
352
353
  context.metrics.typecheck_ms += _elapsed_ms(typecheck_started)
353
354
  return self._failure(context, "preflight", error.display("type-msg").strip())
355
+ except pydantic_monty.MontyRuntimeError as error:
356
+ context.metrics.typecheck_ms += _elapsed_ms(typecheck_started)
357
+ return self._failure(context, "preflight", error.display("type-msg").strip())
354
358
  except RuntimeError as error:
355
359
  context.metrics.typecheck_ms += _elapsed_ms(typecheck_started)
356
360
  return self._failure(
@@ -360,18 +364,22 @@ class MontyExecutor:
360
364
  )
361
365
  context.metrics.typecheck_ms += _elapsed_ms(typecheck_started)
362
366
 
363
- runtime_wrapped = _wrap_code(code, typed=False, has_input=has_input)
364
- runtime_code = _rewrite_sdk_calls(runtime_wrapped, context.catalog)
365
367
  try:
366
- monty = await pydantic_monty.Monty.acreate(
367
- runtime_code,
368
- script_name="codemcp_execute.py",
368
+ monty = await self._compile_runtime(
369
+ code,
370
+ context.catalog,
371
+ has_input=has_input,
369
372
  )
370
- except (pydantic_monty.MontySyntaxError, RuntimeError) as error:
373
+ except (
374
+ pydantic_monty.MontySyntaxError,
375
+ pydantic_monty.MontyRuntimeError,
376
+ RuntimeError,
377
+ NotImplementedError,
378
+ ) as error:
371
379
  return self._failure(
372
380
  context,
373
381
  "preflight",
374
- f"SDK facade compilation failed: {error}",
382
+ f"Sandbox compilation failed: {error}",
375
383
  )
376
384
 
377
385
  async def dispatch_wrapper(name: str, arguments: JsonObject) -> JsonValue:
@@ -530,6 +538,20 @@ class MontyExecutor:
530
538
  response.metrics = context.metrics.model_copy()
531
539
  return response
532
540
 
541
+ @staticmethod
542
+ async def _compile_runtime(
543
+ code: str,
544
+ catalog: ToolCatalog,
545
+ *,
546
+ has_input: bool,
547
+ ) -> pydantic_monty.Monty:
548
+ runtime_wrapped = _wrap_code(code, typed=False, has_input=has_input)
549
+ runtime_code = _rewrite_sdk_calls(runtime_wrapped, catalog)
550
+ return await pydantic_monty.Monty.acreate(
551
+ runtime_code,
552
+ script_name="codemcp_execute.py",
553
+ )
554
+
533
555
  async def _type_check(
534
556
  self,
535
557
  wrapped_code: str,
@@ -727,11 +727,22 @@ class GatewayRuntime:
727
727
  spec = candidate_catalog.tools[candidate.public_name]
728
728
  try:
729
729
  await self.executor.validate_saved_chain(code, candidate_catalog, spec)
730
- except (pydantic_monty.MontyTypingError, pydantic_monty.MontySyntaxError) as error:
730
+ except (
731
+ pydantic_monty.MontyTypingError,
732
+ pydantic_monty.MontySyntaxError,
733
+ pydantic_monty.MontyRuntimeError,
734
+ NotImplementedError,
735
+ RuntimeError,
736
+ ) as error:
731
737
  if isinstance(error, pydantic_monty.MontyTypingError):
732
738
  message = error.display("concise", color=False).strip()
733
- else:
739
+ elif isinstance(
740
+ error,
741
+ (pydantic_monty.MontySyntaxError, pydantic_monty.MontyRuntimeError),
742
+ ):
734
743
  message = error.display("type-msg").strip()
744
+ else:
745
+ message = f"Sandbox compilation failed: {error}"
735
746
  raise ValueError(_saved_chain_preflight_error(message, output_schema)) from error
736
747
  dependencies = self._chain_dependencies(code, candidate_catalog)
737
748
  saved = ChainStore.build(
@@ -790,11 +801,22 @@ class GatewayRuntime:
790
801
  spec = candidate_catalog.tools[current.public_name]
791
802
  try:
792
803
  await self.executor.validate_saved_chain(current.code, candidate_catalog, spec)
793
- except (pydantic_monty.MontyTypingError, pydantic_monty.MontySyntaxError) as error:
804
+ except (
805
+ pydantic_monty.MontyTypingError,
806
+ pydantic_monty.MontySyntaxError,
807
+ pydantic_monty.MontyRuntimeError,
808
+ NotImplementedError,
809
+ RuntimeError,
810
+ ) as error:
794
811
  if isinstance(error, pydantic_monty.MontyTypingError):
795
812
  message = error.display("concise", color=False).strip()
796
- else:
813
+ elif isinstance(
814
+ error,
815
+ (pydantic_monty.MontySyntaxError, pydantic_monty.MontyRuntimeError),
816
+ ):
797
817
  message = error.display("type-msg").strip()
818
+ else:
819
+ message = f"Sandbox compilation failed: {error}"
798
820
  raise ValueError(
799
821
  _saved_chain_preflight_error(message, current.output_schema)
800
822
  ) from error
package/src/modal.ts CHANGED
@@ -124,6 +124,7 @@ interface ManagerSaveResult {
124
124
  }
125
125
 
126
126
  type UnsavedAction = "save" | "discard" | "cancel";
127
+ export type ServerManagerResult = "report-problem" | undefined;
127
128
 
128
129
  interface ServerManagerOptions {
129
130
  servers: ServerModalState[];
@@ -159,6 +160,11 @@ const OVERLAY_OPTIONS = {
159
160
  maxHeight: "85%",
160
161
  } as const;
161
162
 
163
+ const PROBLEM_REPORT_LABEL = "Extension is broken!";
164
+ const PROBLEM_REPORT_DESCRIPTION =
165
+ "Well, that sucks. With this button you can ask the agent to describe the problem and prepare a GitHub issue for review. The goal is to make pi-codemcp usable for everyone, don't be lazy - submit an issue. Don't worry, you will see all prompts, this is a transparent process.";
166
+ const PROBLEM_REPORT_SHORTCUT = "Report issue: R";
167
+
162
168
  const SETTING_DEFINITIONS: SettingDefinition[] = [
163
169
  {
164
170
  key: "backgroundWarmup",
@@ -214,18 +220,18 @@ const SETTING_DEFINITIONS: SettingDefinition[] = [
214
220
  export async function showServerManagerModal(
215
221
  ctx: ExtensionCommandContext,
216
222
  options: ServerManagerOptions,
217
- ): Promise<void> {
223
+ ): Promise<ServerManagerResult> {
218
224
  if (ctx.mode !== "tui") {
219
225
  throw new Error("CodeMCP server manager requires interactive mode");
220
226
  }
221
227
 
222
- await ctx.ui.custom<void>(
228
+ return ctx.ui.custom<ServerManagerResult>(
223
229
  (tui, theme, keybindings, done) =>
224
230
  new ServerManagerModal(
225
231
  options,
226
232
  theme,
227
233
  keybindings,
228
- () => done(undefined),
234
+ (result) => done(result),
229
235
  () => tui.requestRender(),
230
236
  ),
231
237
  { overlay: true, overlayOptions: OVERLAY_OPTIONS },
@@ -352,7 +358,7 @@ class ServerManagerModal implements Component, Focusable {
352
358
  private readonly options: ServerManagerOptions,
353
359
  private readonly theme: Theme,
354
360
  private readonly keybindings: Keybindings,
355
- private readonly close: () => void,
361
+ private readonly close: (result?: ServerManagerResult) => void,
356
362
  private readonly requestRender: () => void,
357
363
  ) {
358
364
  this.savedSettings = cloneSettings(options.settings);
@@ -400,6 +406,11 @@ class ServerManagerModal implements Component, Focusable {
400
406
  return;
401
407
  }
402
408
  if (this.settingsBusy || this.closePromptBusy) return;
409
+ if (data === "R") {
410
+ this.focusProblemReport();
411
+ this.requestRender();
412
+ return;
413
+ }
403
414
  if (this.keybindings.matches(data, "tui.select.cancel") || matchesKey(data, Key.escape)) {
404
415
  if (this.activeTab !== "settings" && this.search.getValue()) {
405
416
  this.search.setValue("");
@@ -504,7 +515,7 @@ class ServerManagerModal implements Component, Focusable {
504
515
  this.selectedSettingIndex = cycleIndex(
505
516
  this.selectedSettingIndex,
506
517
  -1,
507
- SETTING_DEFINITIONS.length,
518
+ SETTING_DEFINITIONS.length + 1,
508
519
  );
509
520
  return;
510
521
  }
@@ -512,10 +523,14 @@ class ServerManagerModal implements Component, Focusable {
512
523
  this.selectedSettingIndex = cycleIndex(
513
524
  this.selectedSettingIndex,
514
525
  1,
515
- SETTING_DEFINITIONS.length,
526
+ SETTING_DEFINITIONS.length + 1,
516
527
  );
517
528
  return;
518
529
  }
530
+ if (this.selectedSettingIndex === SETTING_DEFINITIONS.length) {
531
+ if (matchesKey(data, Key.enter) || data === " ") this.openProblemReport();
532
+ return;
533
+ }
519
534
  if (matchesKey(data, Key.left)) this.cycleSelectedSetting(-1);
520
535
  else if (matchesKey(data, Key.right) || matchesKey(data, Key.enter) || data === " ") {
521
536
  this.cycleSelectedSetting(1);
@@ -820,27 +835,50 @@ class ServerManagerModal implements Component, Focusable {
820
835
  ),
821
836
  );
822
837
  }
838
+ const problemReportSelected = this.selectedSettingIndex === SETTING_DEFINITIONS.length;
839
+ const problemReportPrefix = problemReportSelected ? this.theme.fg("accent", "→") : " ";
840
+ left.push(
841
+ "",
842
+ truncateToWidth(
843
+ `${problemReportPrefix} ${
844
+ problemReportSelected
845
+ ? this.theme.fg("accent", PROBLEM_REPORT_LABEL)
846
+ : PROBLEM_REPORT_LABEL
847
+ }`,
848
+ leftWidth,
849
+ ),
850
+ );
823
851
  const definition = SETTING_DEFINITIONS[this.selectedSettingIndex];
824
- const right = definition
852
+ const right = problemReportSelected
825
853
  ? [
826
- this.theme.fg("accent", this.theme.bold(definition.label)),
827
- this.theme.fg("muted", settingLabel(definition, this.draftSettings[definition.key])),
854
+ this.theme.fg("accent", this.theme.bold(PROBLEM_REPORT_LABEL)),
828
855
  "",
829
- ...wrapPlainText(definition.description, rightWidth).map((line) =>
856
+ ...wrapPlainText(PROBLEM_REPORT_DESCRIPTION, rightWidth).map((line) =>
830
857
  this.theme.fg("muted", line),
831
858
  ),
832
859
  "",
833
- this.theme.fg("dim", "←/→ change · enter next · ctrl+s save"),
834
- ...(this.settingsBusy
835
- ? ["", this.theme.fg("warning", "Saving staged changes…")]
836
- : this.hasUnsavedChanges()
837
- ? ["", this.theme.fg("warning", "Unsaved changes")]
838
- : []),
839
- ...(this.settingsError
840
- ? ["", this.theme.fg("warning", `Error: ${this.settingsError}`)]
841
- : []),
860
+ this.theme.fg("dim", "enter describe the problem"),
842
861
  ]
843
- : [];
862
+ : definition
863
+ ? [
864
+ this.theme.fg("accent", this.theme.bold(definition.label)),
865
+ this.theme.fg("muted", settingLabel(definition, this.draftSettings[definition.key])),
866
+ "",
867
+ ...wrapPlainText(definition.description, rightWidth).map((line) =>
868
+ this.theme.fg("muted", line),
869
+ ),
870
+ "",
871
+ this.theme.fg("dim", "←/→ change · enter next · ctrl+s save"),
872
+ ...(this.settingsBusy
873
+ ? ["", this.theme.fg("warning", "Saving staged changes…")]
874
+ : this.hasUnsavedChanges()
875
+ ? ["", this.theme.fg("warning", "Unsaved changes")]
876
+ : []),
877
+ ...(this.settingsError
878
+ ? ["", this.theme.fg("warning", `Error: ${this.settingsError}`)]
879
+ : []),
880
+ ]
881
+ : [];
844
882
  const lines: string[] = [];
845
883
  for (let index = 0; index < splitHeight; index += 1) {
846
884
  lines.push(
@@ -852,16 +890,17 @@ class ServerManagerModal implements Component, Focusable {
852
890
 
853
891
  private footer(): string {
854
892
  const pending = this.hasUnsavedChanges() ? " · * unsaved · ctrl+s save" : "";
893
+ const report = ` · ${PROBLEM_REPORT_SHORTCUT}`;
855
894
  if (this.activeTab === "settings") {
856
- return `tab servers · ↑/↓ navigate · ←/→/enter change · ctrl+s save · esc close${pending}`;
895
+ return `tab servers · ↑/↓ navigate · ←/→ change · enter select · ctrl+s save · esc close${pending}${report}`;
857
896
  }
858
897
  if (this.activeTab === "chains") {
859
- return `tab stats · ↑/↓ navigate · space toggle · r revalidate · del delete · esc close${pending}`;
898
+ return `tab stats · ↑/↓ navigate · space toggle · r revalidate · del delete · esc close${pending}${report}`;
860
899
  }
861
900
  if (this.activeTab === "stats") {
862
- return `tab settings · bounded local rollups · esc close${pending}`;
901
+ return `tab settings · bounded local rollups · esc close${pending}${report}`;
863
902
  }
864
- return `tab chains · ←/→ pane · ↑/↓ navigate · space toggle · d discover · esc close${pending}`;
903
+ return `tab chains · ←/→ pane · ↑/↓ navigate · space toggle · d discover · esc close${pending}${report}`;
865
904
  }
866
905
 
867
906
  private moveSelection(direction: -1 | 1): void {
@@ -1053,17 +1092,29 @@ class ServerManagerModal implements Component, Focusable {
1053
1092
  }
1054
1093
  }
1055
1094
 
1056
- private resolveUnsavedClose(): void {
1095
+ private focusProblemReport(): void {
1096
+ this.activeTab = "settings";
1097
+ this.selectedSettingIndex = SETTING_DEFINITIONS.length;
1098
+ this.search.setValue("");
1099
+ this.search.focused = false;
1100
+ }
1101
+
1102
+ private openProblemReport(): void {
1103
+ if (this.hasUnsavedChanges()) this.resolveUnsavedClose("report-problem");
1104
+ else this.close("report-problem");
1105
+ }
1106
+
1107
+ private resolveUnsavedClose(result?: ServerManagerResult): void {
1057
1108
  if (this.closePromptBusy || this.settingsBusy) return;
1058
1109
  this.closePromptBusy = true;
1059
1110
  void this.options
1060
1111
  .onResolveUnsaved()
1061
1112
  .then(async (action) => {
1062
1113
  if (action === "discard") {
1063
- this.close();
1114
+ this.close(result);
1064
1115
  return;
1065
1116
  }
1066
- if (action === "save" && (await this.persistDraft())) this.close();
1117
+ if (action === "save" && (await this.persistDraft())) this.close(result);
1067
1118
  })
1068
1119
  .catch((error: unknown) => {
1069
1120
  this.settingsError = summarizeError(error);
package/src/prompts.ts CHANGED
@@ -11,11 +11,12 @@ export const EXECUTE_PROMPT_GUIDELINES = [
11
11
  "Use programmatic execution for a bounded workflow when code can deterministically filter, join, aggregate, deduplicate, validate, or reduce intermediate results.",
12
12
  "Keep a model turn between calls when an intermediate result changes the semantic decision or user approval is required.",
13
13
  "Return the smallest result that answers the request; oversized results fail explicitly with bounded structural inspection data.",
14
- "SDK facades returned by search are prebound globals and must not be imported; use a normal import statement such as `import asyncio` before `asyncio.gather`, because `__import__` is unavailable.",
14
+ "SDK facades are prebound globals and must not be imported. Import supported stdlib normally (for example, `import asyncio`); class declarations and `__import__` are unsupported.",
15
15
  ] as const;
16
16
 
17
17
  export const SAVE_CHAIN_PROMPT_GUIDELINES = [
18
18
  "Save only after the user explicitly asks or accepts, and only after the same code has executed successfully.",
19
+ "Use generated result item types for nested schema collections; do not declare TypedDict classes.",
19
20
  "Use project scope when available unless the user explicitly requests global scope; make schemas describe the exact parameterized contract.",
20
21
  ] as const;
21
22