pi-codemcp 1.3.3 → 1.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
@@ -7,6 +7,7 @@ Instead of putting every upstream MCP tool definition into the model context, pi
7
7
  - `codemcp_search` ranks capabilities or pages through a compact inventory without loading full schemas.
8
8
  - `codemcp_inspect` returns exact typed SDK stubs only for selected calls.
9
9
  - `codemcp_execute` runs one sandboxed Python call graph across one or many MCP servers.
10
+ - `codemcp_edit` applies one exact replacement to the previous execution and reruns it without resending the full code.
10
11
  - `codemcp_save_chain` turns a repeated call graph into a reusable native Pi tool.
11
12
  - `codemcp_manage_chains` lists chains or performs an explicitly confirmed enable, disable, revalidate, or delete.
12
13
 
@@ -177,6 +178,8 @@ Incomplete upstream schemas become recursive `JsonValue`, not `Any`; use the pre
177
178
 
178
179
  When an oversized value fits the bounded in-memory refinement cache, the failure also returns an opaque `result_ref` and expiry. Pass that reference back as `inputRef` on one follow-up `codemcp_execute`; the retained JSON is exposed as `input`, so code can filter or aggregate it without repeating upstream calls. References expire after five minutes, are valid only in the originating sidecar, and are never persisted.
179
180
 
181
+ For a small correction, `codemcp_edit` replaces one uniquely matching `oldText` with `newText` in the most recent execution and reruns it with the same `inputRef`. The state is in-memory and disappears when the sidecar restarts. The full call graph runs again, including upstream MCP calls.
182
+
180
183
  ## Saved-chain CLI flow
181
184
 
182
185
  Saved chains are JSON manifests with sandboxed code plus explicit input/output JSON Schemas. Project-scoped chains live under `<project>/.pi/pi-codemcp/chains`; global chains live under `<agent-dir>/pi-codemcp/chains`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codemcp",
3
- "version": "1.3.3",
3
+ "version": "1.4.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",
@@ -206,6 +206,11 @@ class SavedChainHandlers(NamedTuple):
206
206
  delete: Callable[[str, ChainScope, str], Awaitable[ChainListResponse]]
207
207
 
208
208
 
209
+ class _LastExecution(NamedTuple):
210
+ code: str
211
+ input_ref: str | None
212
+
213
+
209
214
  class SavedChainRuntime:
210
215
  def __init__(self, handlers: SavedChainHandlers) -> None:
211
216
  self.handlers = handlers
@@ -307,6 +312,8 @@ class GatewayRuntime:
307
312
  )
308
313
  )
309
314
  self._catalog_lock = asyncio.Lock()
315
+ self._execute_lock = asyncio.Lock()
316
+ self._last_execution: _LastExecution | None = None
310
317
 
311
318
  @classmethod
312
319
  def create(
@@ -595,6 +602,38 @@ class GatewayRuntime:
595
602
  trace_id: str,
596
603
  input_ref: str | None = None,
597
604
  ) -> ExecutionResponse:
605
+ async with self._execute_lock:
606
+ return await self._execute(code, trace_id, input_ref)
607
+
608
+ async def edit_execute(
609
+ self,
610
+ old_text: str,
611
+ new_text: str,
612
+ trace_id: str,
613
+ ) -> ExecutionResponse:
614
+ async with self._execute_lock:
615
+ previous = self._last_execution
616
+ if previous is None:
617
+ raise ValueError("No previous CodeMCP execution is available in this sidecar")
618
+ if not old_text:
619
+ raise ValueError("old_text must not be empty")
620
+ if old_text == new_text:
621
+ raise ValueError("old_text and new_text must differ")
622
+ matches = previous.code.count(old_text)
623
+ if matches != 1:
624
+ raise ValueError(
625
+ f"old_text must match the previous code exactly once; found {matches} matches"
626
+ )
627
+ code = previous.code.replace(old_text, new_text, 1)
628
+ return await self._execute(code, trace_id, previous.input_ref)
629
+
630
+ async def _execute(
631
+ self,
632
+ code: str,
633
+ trace_id: str,
634
+ input_ref: str | None,
635
+ ) -> ExecutionResponse:
636
+ self._last_execution = None
598
637
  started = time.perf_counter()
599
638
  input_bytes = len(code.encode()) + len((input_ref or "").encode())
600
639
  input_value: JsonValue = None
@@ -619,6 +658,7 @@ class GatewayRuntime:
619
658
  response,
620
659
  trace_id,
621
660
  )
661
+ self._last_execution = _LastExecution(code, input_ref)
622
662
  return response
623
663
  discovery_started = time.perf_counter()
624
664
  try:
@@ -655,6 +695,7 @@ class GatewayRuntime:
655
695
  )
656
696
  raise
657
697
  self._record_execution("execute", started, input_bytes, response, trace_id)
698
+ self._last_execution = _LastExecution(code, input_ref)
658
699
  return response
659
700
 
660
701
  async def _execute_chain(
@@ -1679,6 +1720,16 @@ async def execute(
1679
1720
  return await _require_runtime().execute(code, trace_id, input_ref)
1680
1721
 
1681
1722
 
1723
+ @mcp.tool
1724
+ async def edit_execute(
1725
+ trace_id: str,
1726
+ old_text: str,
1727
+ new_text: str,
1728
+ ) -> ExecutionResponse:
1729
+ """Patch and rerun the last sandbox execution."""
1730
+ return await _require_runtime().edit_execute(old_text, new_text, trace_id)
1731
+
1732
+
1682
1733
  @mcp.tool
1683
1734
  async def save_chain(
1684
1735
  trace_id: str,
package/src/mcp-client.ts CHANGED
@@ -23,6 +23,7 @@ export type SidecarToolName =
23
23
  | "discover"
24
24
  | "reload_settings"
25
25
  | "execute"
26
+ | "edit_execute"
26
27
  | "save_chain"
27
28
  | "list_chains"
28
29
  | "execute_chain"
@@ -34,6 +35,7 @@ export type SidecarToolName =
34
35
 
35
36
  const LONG_RUNNING_TOOLS = new Set<SidecarToolName>([
36
37
  "execute",
38
+ "edit_execute",
37
39
  "save_chain",
38
40
  "execute_chain",
39
41
  "revalidate_chain",
package/src/tools.ts CHANGED
@@ -152,6 +152,16 @@ const ExecuteParameters = Type.Object({
152
152
  ),
153
153
  });
154
154
 
155
+ const EditExecuteParameters = Type.Object({
156
+ oldText: Type.String({
157
+ minLength: 1,
158
+ description: "Exact text that must occur once in the previous execution code",
159
+ }),
160
+ newText: Type.String({
161
+ description: "Replacement text; may be empty",
162
+ }),
163
+ });
164
+
155
165
  export function registerCodeMcpTools(
156
166
  pi: ExtensionAPI,
157
167
  lifecycle: CodeMcpLifecycle,
@@ -321,32 +331,7 @@ export function registerCodeMcpTools(
321
331
  },
322
332
  signal,
323
333
  );
324
- const ok = result.ok === true;
325
- const modelValue = ok
326
- ? result.result
327
- : {
328
- failure_stage: result.failure_stage,
329
- error: result.error,
330
- failure: result.failure,
331
- shape: result.shape,
332
- result_ref: result.result_ref,
333
- expires_in_seconds: result.expires_in_seconds,
334
- calls_made: result.calls_made,
335
- chain_calls: result.chain_calls,
336
- };
337
- const output = formatCodeMcpOutput(modelValue, outputLimits(lifecycle));
338
- return {
339
- content: [{ type: "text", text: output.text }],
340
- details: {
341
- ...output.details,
342
- ok,
343
- failureStage: typeof result.failure_stage === "string" ? result.failure_stage : undefined,
344
- callsMade: Number(result.calls_made ?? 0),
345
- chainCalls: Number(result.chain_calls ?? 0),
346
- ...(isRecord(result.timings) ? { timings: result.timings } : {}),
347
- preview: previewExecutionValue(ok ? result.result : result.error),
348
- },
349
- };
334
+ return formatExecutionResponse(result, lifecycle);
350
335
  },
351
336
  renderCall(args, theme, context) {
352
337
  const code = args.code.trim();
@@ -379,6 +364,46 @@ export function registerCodeMcpTools(
379
364
  },
380
365
  });
381
366
 
367
+ pi.registerTool({
368
+ name: "codemcp_edit",
369
+ label: "MCP Edit",
370
+ description:
371
+ "Patch and rerun the most recent codemcp_execute in the current sidecar without resending its full code. oldText must match exactly once. The original inputRef is reused and the entire call graph, including upstream MCP calls, runs again.",
372
+ promptSnippet: "Patch and rerun the previous CodeMCP execution",
373
+ parameters: EditExecuteParameters,
374
+ async execute(toolCallId, params, signal, onUpdate) {
375
+ onUpdate?.({
376
+ content: [{ type: "text", text: "Patching and type-checking MCP chain..." }],
377
+ details: undefined,
378
+ });
379
+ const result = await lifecycle.request(
380
+ "edit_execute",
381
+ {
382
+ old_text: params.oldText,
383
+ new_text: params.newText,
384
+ trace_id: toolCallId,
385
+ },
386
+ signal,
387
+ );
388
+ return formatExecutionResponse(result, lifecycle);
389
+ },
390
+ renderCall(args, theme) {
391
+ const lines = args.oldText ? args.oldText.split("\n").length : 0;
392
+ const title = theme.fg("toolTitle", theme.bold("MCP Edit"));
393
+ const patchLabel = theme.fg(
394
+ "accent",
395
+ theme.bold(`Edited ${lines} ${lines === 1 ? "line" : "lines"}`),
396
+ );
397
+ return new Text(`${title} ${theme.fg("muted", "·")} ${patchLabel}`, 0, 0);
398
+ },
399
+ renderResult(result, state, theme) {
400
+ return renderExecutionResult(result, state, theme, {
401
+ partialText: "Applying edit, then execution...",
402
+ expandDescription: "full output",
403
+ });
404
+ },
405
+ });
406
+
382
407
  pi.registerTool({
383
408
  name: "codemcp_save_chain",
384
409
  label: "Save MCP Chain",
@@ -552,6 +577,35 @@ export function registerCodeMcpTools(
552
577
  });
553
578
  }
554
579
 
580
+ function formatExecutionResponse(result: Record<string, unknown>, lifecycle: CodeMcpLifecycle) {
581
+ const ok = result.ok === true;
582
+ const modelValue = ok
583
+ ? result.result
584
+ : {
585
+ failure_stage: result.failure_stage,
586
+ error: result.error,
587
+ failure: result.failure,
588
+ shape: result.shape,
589
+ result_ref: result.result_ref,
590
+ expires_in_seconds: result.expires_in_seconds,
591
+ calls_made: result.calls_made,
592
+ chain_calls: result.chain_calls,
593
+ };
594
+ const output = formatCodeMcpOutput(modelValue, outputLimits(lifecycle));
595
+ return {
596
+ content: [{ type: "text" as const, text: output.text }],
597
+ details: {
598
+ ...output.details,
599
+ ok,
600
+ failureStage: typeof result.failure_stage === "string" ? result.failure_stage : undefined,
601
+ callsMade: Number(result.calls_made ?? 0),
602
+ chainCalls: Number(result.chain_calls ?? 0),
603
+ ...(isRecord(result.timings) ? { timings: result.timings } : {}),
604
+ preview: previewExecutionValue(ok ? result.result : result.error),
605
+ },
606
+ };
607
+ }
608
+
555
609
  function compactChainView(view: SavedChainView) {
556
610
  return {
557
611
  name: view.chain.name,