pi-codemcp 1.4.0 → 1.5.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.
package/src/settings.ts CHANGED
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { readJsonObject, requireJsonObject, writeJsonObjectAtomically } from "./json-file.js";
3
3
 
4
4
  export interface CodeMcpSettings {
5
+ jevEnabled: boolean;
5
6
  backgroundWarmup: boolean;
6
7
  cacheTtlHours: number;
7
8
  executionTimeoutSeconds: number;
@@ -16,6 +17,7 @@ export type EditableSettingKey = Exclude<keyof CodeMcpSettings, "disabledTools">
16
17
  export type EditableSettingValue = boolean | number;
17
18
 
18
19
  export const DEFAULT_CODEMCP_SETTINGS: Readonly<CodeMcpSettings> = {
20
+ jevEnabled: false,
19
21
  backgroundWarmup: true,
20
22
  cacheTtlHours: 24,
21
23
  executionTimeoutSeconds: 30,
@@ -28,6 +30,7 @@ export const DEFAULT_CODEMCP_SETTINGS: Readonly<CodeMcpSettings> = {
28
30
 
29
31
  const ALLOWED_KEYS = new Set([
30
32
  "version",
33
+ "jevEnabled",
31
34
  "backgroundWarmup",
32
35
  "cacheTtlHours",
33
36
  "executionTimeoutSeconds",
@@ -45,16 +48,18 @@ export function loadCodeMcpSettings(path: string): CodeMcpSettings {
45
48
  if (version !== 1 && version !== 2) {
46
49
  throw new Error(`Unsupported CodeMCP settings version: ${String(version)}`);
47
50
  }
48
- const migrated =
51
+ const versionMigrated =
49
52
  version === 1
50
53
  ? Object.fromEntries(Object.entries(root).filter(([key]) => key !== "outputLineLimit"))
51
54
  : root;
55
+ const migrated = migrateDiscoveryMode(versionMigrated);
52
56
  const unknown = Object.keys(migrated).filter((key) => !ALLOWED_KEYS.has(key));
53
57
  if (unknown.length > 0) {
54
58
  throw new Error(`Unknown CodeMCP settings: ${unknown.join(", ")}`);
55
59
  }
56
60
 
57
61
  return {
62
+ jevEnabled: booleanSetting(migrated, "jevEnabled"),
58
63
  backgroundWarmup: booleanSetting(migrated, "backgroundWarmup"),
59
64
  cacheTtlHours: integerSetting(migrated, "cacheTtlHours", 0, 720),
60
65
  executionTimeoutSeconds: integerSetting(migrated, "executionTimeoutSeconds", 1, 300),
@@ -69,6 +74,7 @@ export function loadCodeMcpSettings(path: string): CodeMcpSettings {
69
74
  export function saveCodeMcpSettings(path: string, settings: CodeMcpSettings): void {
70
75
  writeJsonObjectAtomically(path, {
71
76
  version: 2,
77
+ jevEnabled: settings.jevEnabled,
72
78
  backgroundWarmup: settings.backgroundWarmup,
73
79
  cacheTtlHours: settings.cacheTtlHours,
74
80
  executionTimeoutSeconds: settings.executionTimeoutSeconds,
@@ -85,7 +91,7 @@ export function setEditableSetting(
85
91
  key: EditableSettingKey,
86
92
  value: EditableSettingValue,
87
93
  ): CodeMcpSettings {
88
- if (key === "backgroundWarmup") {
94
+ if (key === "jevEnabled" || key === "backgroundWarmup") {
89
95
  if (typeof value !== "boolean") throw new TypeError(`${key} must be a boolean`);
90
96
  return { ...settings, [key]: value };
91
97
  }
@@ -112,7 +118,21 @@ function cloneDefaults(): CodeMcpSettings {
112
118
  return { ...DEFAULT_CODEMCP_SETTINGS, disabledTools: {} };
113
119
  }
114
120
 
115
- function booleanSetting(root: Record<string, unknown>, key: "backgroundWarmup"): boolean {
121
+ function migrateDiscoveryMode(root: Record<string, unknown>): Record<string, unknown> {
122
+ if (root.discoveryMode === undefined) return root;
123
+ if (root.discoveryMode !== "search" && root.discoveryMode !== "jev") {
124
+ throw new TypeError("discoveryMode must be search or jev");
125
+ }
126
+ return {
127
+ ...Object.fromEntries(Object.entries(root).filter(([key]) => key !== "discoveryMode")),
128
+ jevEnabled: root.jevEnabled ?? root.discoveryMode === "jev",
129
+ };
130
+ }
131
+
132
+ function booleanSetting(
133
+ root: Record<string, unknown>,
134
+ key: "jevEnabled" | "backgroundWarmup",
135
+ ): boolean {
116
136
  const value = root[key] ?? DEFAULT_CODEMCP_SETTINGS[key];
117
137
  if (typeof value !== "boolean") throw new TypeError(`${key} must be a boolean`);
118
138
  return value;
@@ -120,7 +140,7 @@ function booleanSetting(root: Record<string, unknown>, key: "backgroundWarmup"):
120
140
 
121
141
  function integerSetting(
122
142
  root: Record<string, unknown>,
123
- key: Exclude<EditableSettingKey, "backgroundWarmup">,
143
+ key: Exclude<EditableSettingKey, "jevEnabled" | "backgroundWarmup">,
124
144
  minimum: number,
125
145
  maximum: number,
126
146
  ): number {
package/src/tools.ts CHANGED
@@ -12,7 +12,9 @@ import {
12
12
  previewExecutionValue,
13
13
  renderExecutionResult,
14
14
  } from "./execution-rendering.js";
15
+ import type { JevRouter } from "./jev-router.js";
15
16
  import type { CodeMcpLifecycle } from "./lifecycle.js";
17
+ import type { SidecarProgress } from "./mcp-client.js";
16
18
  import { type CodeMcpOutputDetails, formatCodeMcpOutput } from "./output.js";
17
19
  import {
18
20
  EXECUTE_PROMPT_GUIDELINES,
@@ -73,6 +75,15 @@ const SearchParameters = Type.Object({
73
75
  ),
74
76
  });
75
77
 
78
+ const JevRouteParameters = Type.Object({
79
+ intent: Type.String({
80
+ minLength: 1,
81
+ pattern: "\\S",
82
+ description:
83
+ "Briefly describe the current MCP subtask and relevant service or environment, e.g. read staging logs to diagnose an approval error. Not instructions to another agent; the original user request is included automatically.",
84
+ }),
85
+ });
86
+
76
87
  const InspectParameters = Type.Object({
77
88
  calls: Type.Array(Type.String({ minLength: 1 }), {
78
89
  minItems: 1,
@@ -162,6 +173,80 @@ const EditExecuteParameters = Type.Object({
162
173
  }),
163
174
  });
164
175
 
176
+ export function registerJevRouteTool(
177
+ pi: ExtensionAPI,
178
+ getRouter: () => Pick<JevRouter, "route"> | undefined,
179
+ ): void {
180
+ pi.registerTool({
181
+ name: "codemcp_route",
182
+ label: "Jev MCP Route",
183
+ description:
184
+ "Use Jev to select configured MCP calls for the agent's current subtask, including prerequisites, classify each call's workflow role, recommend parallel or dependent composition, and return exact typed SDK contracts. Use when the subtask may require external services or saved workflows. If no configured capability applies, returns no calls.",
185
+ promptSnippet: "Select MCP calls and composition guidance for the current subtask with Jev",
186
+ promptGuidelines: [
187
+ "Use codemcp_route once per distinct MCP subtask. Provide a short intent describing what you need now, including relevant findings; the original user request and recent context are included automatically. Route again with an updated intent if the subtask changes or the selected contracts cannot complete it.",
188
+ "After codemcp_route returns contracts, immediately write and run the recommended minimal codemcp_execute program instead of stopping to describe the plan.",
189
+ "Follow codemcp_route composition guidance: gather independent calls, sequence dependent calls, and preserve a model turn only for semantic decisions or approvals.",
190
+ ],
191
+ parameters: JevRouteParameters,
192
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
193
+ const router = getRouter();
194
+ if (!router) throw new Error("Jev routing requires TYPESAFE_API_KEY");
195
+ const { task, recentContext } = currentRouteTask(ctx.sessionManager.buildContextEntries());
196
+ onUpdate?.({
197
+ content: [{ type: "text", text: "Jev is selecting and composing MCP calls..." }],
198
+ details: undefined,
199
+ });
200
+ try {
201
+ const route = await router.route(
202
+ params.intent,
203
+ [`Original user request: ${task}`, recentContext].filter(Boolean).join("\n\n"),
204
+ signal,
205
+ );
206
+ return {
207
+ content: [{ type: "text", text: route.prompt }],
208
+ details: {
209
+ selected: route.selected.map((tool) => ({
210
+ call: tool.call,
211
+ relevance: tool.relevance,
212
+ role: tool.role,
213
+ })),
214
+ needsAnyTool: route.needsAnyTool,
215
+ workflowShape: route.workflowShape,
216
+ needsCheckpoint: route.needsCheckpoint,
217
+ },
218
+ };
219
+ } catch (error) {
220
+ const active = pi.getActiveTools();
221
+ if (!active.includes("codemcp_search")) {
222
+ pi.setActiveTools([...active, "codemcp_search"]);
223
+ }
224
+ throw error;
225
+ }
226
+ },
227
+ renderCall(_args, theme) {
228
+ return new Text(theme.fg("toolTitle", theme.bold("Jev MCP Route")), 0, 0);
229
+ },
230
+ renderResult(result, { expanded, isPartial }, theme) {
231
+ if (isPartial) return new Text(theme.fg("warning", "Jev is routing..."), 0, 0);
232
+ if (expanded) return renderExpandedJson(result.content);
233
+ const details = result.details as
234
+ | { selected?: Array<{ call?: string }>; workflowShape?: string }
235
+ | undefined;
236
+ const selected = details?.selected ?? [];
237
+ let text = theme.fg(
238
+ "success",
239
+ `\n${selected.length} calls · ${details?.workflowShape ?? "no workflow"}`,
240
+ );
241
+ for (const tool of selected.slice(0, 4)) {
242
+ if (tool.call) text += `\n${theme.fg("dim", ` ${tool.call}`)}`;
243
+ }
244
+ text += `\n${theme.fg("muted", keyHint("app.tools.expand", "routing details"))}`;
245
+ return new Text(text, 0, 0);
246
+ },
247
+ });
248
+ }
249
+
165
250
  export function registerCodeMcpTools(
166
251
  pi: ExtensionAPI,
167
252
  lifecycle: CodeMcpLifecycle,
@@ -330,6 +415,7 @@ export function registerCodeMcpTools(
330
415
  ...(params.inputRef === undefined ? {} : { input_ref: params.inputRef }),
331
416
  },
332
417
  signal,
418
+ (progress) => onUpdate?.(executionProgressUpdate(progress)),
333
419
  );
334
420
  return formatExecutionResponse(result, lifecycle);
335
421
  },
@@ -384,6 +470,7 @@ export function registerCodeMcpTools(
384
470
  trace_id: toolCallId,
385
471
  },
386
472
  signal,
473
+ (progress) => onUpdate?.(executionProgressUpdate(progress)),
387
474
  );
388
475
  return formatExecutionResponse(result, lifecycle);
389
476
  },
@@ -577,6 +664,17 @@ export function registerCodeMcpTools(
577
664
  });
578
665
  }
579
666
 
667
+ function executionProgressUpdate(progress: SidecarProgress) {
668
+ const currentCall = progress.message === "executing" ? undefined : progress.message;
669
+ return {
670
+ content: [{ type: "text" as const, text: progress.message ?? "Executing MCP calls..." }],
671
+ details: {
672
+ callsMade: progress.progress,
673
+ ...(currentCall === undefined ? {} : { currentCall }),
674
+ },
675
+ };
676
+ }
677
+
580
678
  function formatExecutionResponse(result: Record<string, unknown>, lifecycle: CodeMcpLifecycle) {
581
679
  const ok = result.ok === true;
582
680
  const modelValue = ok
@@ -636,6 +734,40 @@ function outputLimits(lifecycle: CodeMcpLifecycle): { maxBytes: number } {
636
734
  return { maxBytes: settings.outputLimitKiB * 1024 };
637
735
  }
638
736
 
737
+ function currentRouteTask(entries: readonly unknown[]): {
738
+ task: string;
739
+ recentContext: string;
740
+ } {
741
+ const messages: Array<{ role: "user" | "assistant"; text: string }> = [];
742
+ for (const entry of entries) {
743
+ if (!isRecord(entry) || !isRecord(entry.message)) continue;
744
+ const role = entry.message.role;
745
+ if (role !== "user" && role !== "assistant") continue;
746
+ const text = messageText(entry.message.content).trim();
747
+ if (text) messages.push({ role, text });
748
+ }
749
+ let currentIndex = messages.length - 1;
750
+ while (currentIndex >= 0 && messages[currentIndex]?.role !== "user") currentIndex -= 1;
751
+ const current = messages[currentIndex];
752
+ if (!current) throw new Error("Jev routing requires a text user request");
753
+ const recentContext = messages
754
+ .slice(Math.max(0, currentIndex - 3), currentIndex)
755
+ .map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.text}`)
756
+ .join("\n\n")
757
+ .slice(-6_000);
758
+ return { task: current.text, recentContext };
759
+ }
760
+
761
+ function messageText(content: unknown): string {
762
+ if (typeof content === "string") return content;
763
+ if (!Array.isArray(content)) return "";
764
+ return content
765
+ .flatMap((item) =>
766
+ isRecord(item) && item.type === "text" && typeof item.text === "string" ? [item.text] : [],
767
+ )
768
+ .join("\n");
769
+ }
770
+
639
771
  function truncate(value: string, maxLength: number): string {
640
772
  return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`;
641
773
  }