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/README.md +92 -196
- package/extensions/index.ts +63 -5
- package/package.json +3 -2
- package/sidecar/executor.py +14 -1
- package/sidecar/gateway.py +29 -5
- package/sidecar/settings.py +7 -0
- package/src/execution-rendering.ts +16 -0
- package/src/jev-router.ts +323 -0
- package/src/lifecycle.ts +8 -2
- package/src/mcp-client.ts +15 -1
- package/src/modal.ts +28 -5
- package/src/settings.ts +24 -4
- package/src/tools.ts +132 -0
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
|
|
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
|
|
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
|
}
|