pi-codemcp 1.0.0 → 1.1.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/src/chains.ts CHANGED
@@ -191,9 +191,8 @@ export class SavedChainManager {
191
191
  throw new Error(error);
192
192
  }
193
193
  const settings = manager.lifecycle.loadSettings();
194
- const output = formatCodeMcpOutput(result, {
194
+ const output = formatCodeMcpOutput(result.result, {
195
195
  maxBytes: settings.outputLimitKiB * 1024,
196
- maxLines: settings.outputLineLimit,
197
196
  });
198
197
  return {
199
198
  content: [{ type: "text", text: output.text }],
@@ -7,6 +7,7 @@ export interface ExecutionRenderDetails extends CodeMcpOutputDetails {
7
7
  failureStage?: string;
8
8
  callsMade: number;
9
9
  chainCalls: number;
10
+ timings?: Record<string, unknown>;
10
11
  preview: string[];
11
12
  }
12
13
 
@@ -79,11 +80,13 @@ function renderExpandedResult(
79
80
  details: ExecutionRenderDetails | undefined,
80
81
  theme: Theme,
81
82
  ): Text {
82
- const response = parseJsonObject(getTextContent(content));
83
+ const raw = getTextContent(content);
84
+ const parsed = parseJsonValue(raw);
85
+ const response = isRecord(parsed) ? parsed : undefined;
83
86
  const calls = details?.callsMade ?? 0;
84
87
  const chainCalls = details?.chainCalls ?? 0;
85
88
  if (details?.ok) {
86
- const output = response ? formatJson(response.result) : getTextContent(content);
89
+ const output = parsed === undefined ? raw : formatJson(parsed);
87
90
  const highlighted = highlightCode(output, "json").join("\n");
88
91
  return new Text(
89
92
  `\n${theme.fg("success", theme.bold(`Output · ${formatExecutionCalls(calls, chainCalls)}`))}\n${highlighted}`,
@@ -155,10 +158,9 @@ function formatJson(value: unknown): string {
155
158
  return JSON.stringify(value, null, 2) ?? String(value);
156
159
  }
157
160
 
158
- function parseJsonObject(value: string): Record<string, unknown> | undefined {
161
+ function parseJsonValue(value: string): unknown {
159
162
  try {
160
- const parsed: unknown = JSON.parse(value);
161
- return isRecord(parsed) ? parsed : undefined;
163
+ return JSON.parse(value);
162
164
  } catch {
163
165
  return undefined;
164
166
  }
package/src/mcp-client.ts CHANGED
@@ -19,6 +19,7 @@ type JsonObject = Record<string, unknown>;
19
19
 
20
20
  export type SidecarToolName =
21
21
  | "search"
22
+ | "inspect"
22
23
  | "discover"
23
24
  | "reload_settings"
24
25
  | "apply_manager_changes"
@@ -28,6 +29,7 @@ export type SidecarToolName =
28
29
  | "execute_chain"
29
30
  | "revalidate_chain"
30
31
  | "delete_chain"
32
+ | "stats"
31
33
  | "status";
32
34
 
33
35
  const LONG_RUNNING_TOOLS = new Set<SidecarToolName>([
@@ -185,7 +187,9 @@ export class SidecarClient {
185
187
  "--frozen",
186
188
  "--no-dev",
187
189
  "-m",
188
- "sidecar.gateway",
190
+ "sidecar.cli",
191
+ "serve",
192
+ "--stdio",
189
193
  ],
190
194
  cwd: this.packageRoot,
191
195
  env: this.environment,
package/src/modal.ts CHANGED
@@ -49,6 +49,43 @@ export interface ChainModalState {
49
49
  error?: string;
50
50
  }
51
51
 
52
+ export interface StatsRollupState {
53
+ count: number;
54
+ success: number;
55
+ failure: number;
56
+ inputBytes: number;
57
+ outputBytes: number;
58
+ calls: number;
59
+ chainCalls: number;
60
+ averageMs: number;
61
+ p50Ms: number;
62
+ p95Ms: number;
63
+ maxMs: number;
64
+ p50OutputBytes: number;
65
+ p95OutputBytes: number;
66
+ }
67
+
68
+ export interface StatsModalState {
69
+ updatedAt: number;
70
+ lifetime: StatsRollupState;
71
+ recent: StatsRollupState;
72
+ operations: Array<{ name: string; rollup: StatsRollupState }>;
73
+ phases: Array<{
74
+ name: string;
75
+ count: number;
76
+ averageMs: number;
77
+ p50Ms: number;
78
+ p95Ms: number;
79
+ maxMs: number;
80
+ }>;
81
+ failures: Array<{ stage: string; count: number }>;
82
+ upstreamOutputBytes: number;
83
+ cacheHits: number;
84
+ cacheMisses: number;
85
+ serverCount: number;
86
+ toolCount: number;
87
+ }
88
+
52
89
  export interface ServerModalState {
53
90
  name: string;
54
91
  transport: string;
@@ -92,6 +129,7 @@ interface ServerManagerOptions {
92
129
  servers: ServerModalState[];
93
130
  chains: ChainModalState[];
94
131
  settings: CodeMcpSettings;
132
+ stats: StatsModalState;
95
133
  onDiscover(server: ServerModalState): Promise<ServerModalState>;
96
134
  onSaveChanges(
97
135
  settings: CodeMcpSettings,
@@ -171,12 +209,6 @@ const SETTING_DEFINITIONS: SettingDefinition[] = [
171
209
  description: "Maximum CodeMCP tool-result text placed into the agent context.",
172
210
  choices: [10, 25, 50, 100, 200, 512].map(kibChoice),
173
211
  },
174
- {
175
- key: "outputLineLimit",
176
- label: "Agent line limit",
177
- description: "Maximum CodeMCP tool-result lines placed into the agent context.",
178
- choices: [500, 1_000, 2_000, 5_000, 10_000].map(numberChoice),
179
- },
180
212
  ];
181
213
 
182
214
  export async function showServerManagerModal(
@@ -221,6 +253,58 @@ export function chainStatesFromViews(views: SavedChainView[]): ChainModalState[]
221
253
  }));
222
254
  }
223
255
 
256
+ export function statsStateFromSnapshot(snapshot: Record<string, unknown>): StatsModalState {
257
+ const lifetime = parseStatsRollup(snapshot.lifetime);
258
+ const recent = Array.isArray(snapshot.recent)
259
+ ? snapshot.recent.reduce<StatsRollupState>(
260
+ (total, item) => addStatsRollups(total, parseStatsRollup(item)),
261
+ emptyStatsRollup(),
262
+ )
263
+ : emptyStatsRollup();
264
+ const operations = parseNamedStatsRollups(snapshot.operations);
265
+ const rawPhases = isRecord(snapshot.phases) ? snapshot.phases : {};
266
+ const phases = Object.entries(rawPhases).flatMap(([name, value]) => {
267
+ if (!isRecord(value)) return [];
268
+ return [
269
+ {
270
+ name,
271
+ count: numberField(value, "count"),
272
+ averageMs: numberField(value, "average"),
273
+ p50Ms: histogramPercentile(value, 0.5),
274
+ p95Ms: histogramPercentile(value, 0.95),
275
+ maxMs: numberField(value, "max"),
276
+ },
277
+ ];
278
+ });
279
+ const rawFailures = isRecord(snapshot.failures) ? snapshot.failures : {};
280
+ const failures = Object.entries(rawFailures)
281
+ .flatMap(([stage, count]) =>
282
+ typeof count === "number" && count >= 0 ? [{ stage, count }] : [],
283
+ )
284
+ .sort((left, right) => right.count - left.count || left.stage.localeCompare(right.stage));
285
+ const rawServers = isRecord(snapshot.servers) ? snapshot.servers : {};
286
+ const upstreamOutputBytes = Object.values(rawServers).reduce<number>(
287
+ (total, value) => total + (isRecord(value) ? numberField(value, "output_bytes") : 0),
288
+ 0,
289
+ );
290
+ const cache = isRecord(snapshot.cache) ? snapshot.cache : {};
291
+ const servers = Object.keys(rawServers).length;
292
+ const tools = isRecord(snapshot.tools) ? Object.keys(snapshot.tools).length : 0;
293
+ return {
294
+ updatedAt: numberField(snapshot, "updated_at"),
295
+ lifetime,
296
+ recent,
297
+ operations,
298
+ phases,
299
+ failures,
300
+ upstreamOutputBytes,
301
+ cacheHits: numberField(cache, "hits"),
302
+ cacheMisses: numberField(cache, "misses"),
303
+ serverCount: servers,
304
+ toolCount: tools,
305
+ };
306
+ }
307
+
224
308
  export function serverStatesFromStatus(status: Record<string, unknown>): ServerModalState[] {
225
309
  if (!Array.isArray(status.upstreams)) return [];
226
310
  return status.upstreams.flatMap((value) => {
@@ -249,7 +333,7 @@ export function serverStatesFromStatus(status: Record<string, unknown>): ServerM
249
333
 
250
334
  class ServerManagerModal implements Component, Focusable {
251
335
  private readonly search = new Input();
252
- private activeTab: "servers" | "chains" | "settings" = "servers";
336
+ private activeTab: "servers" | "chains" | "stats" | "settings" = "servers";
253
337
  private activePane: "servers" | "tools" = "servers";
254
338
  private selectedServerIndex = 0;
255
339
  private selectedToolIndex = 0;
@@ -284,7 +368,7 @@ class ServerManagerModal implements Component, Focusable {
284
368
 
285
369
  set focused(value: boolean) {
286
370
  this._focused = value;
287
- this.search.focused = value && this.activeTab !== "settings";
371
+ this.search.focused = value && !["stats", "settings"].includes(this.activeTab);
288
372
  }
289
373
 
290
374
  render(width: number): string[] {
@@ -297,6 +381,7 @@ class ServerManagerModal implements Component, Focusable {
297
381
  render: (bodyWidth: number) => {
298
382
  if (this.activeTab === "servers") return this.renderServers(bodyWidth);
299
383
  if (this.activeTab === "chains") return this.renderChains(bodyWidth);
384
+ if (this.activeTab === "stats") return this.renderStats(bodyWidth);
300
385
  return this.renderSettings(bodyWidth);
301
386
  },
302
387
  invalidate: () => this.search.invalidate(),
@@ -331,16 +416,18 @@ class ServerManagerModal implements Component, Focusable {
331
416
  this.activeTab === "servers"
332
417
  ? "chains"
333
418
  : this.activeTab === "chains"
334
- ? "settings"
335
- : "servers";
419
+ ? "stats"
420
+ : this.activeTab === "stats"
421
+ ? "settings"
422
+ : "servers";
336
423
  this.search.setValue("");
337
- this.search.focused = this._focused && this.activeTab !== "settings";
424
+ this.search.focused = this._focused && !["stats", "settings"].includes(this.activeTab);
338
425
  this.requestRender();
339
426
  return;
340
427
  }
341
428
  if (this.activeTab === "settings") this.handleSettingsInput(data);
342
429
  else if (this.activeTab === "chains") this.handleChainInput(data);
343
- else this.handleServerInput(data);
430
+ else if (this.activeTab === "servers") this.handleServerInput(data);
344
431
  this.requestRender();
345
432
  }
346
433
 
@@ -444,11 +531,15 @@ class ServerManagerModal implements Component, Focusable {
444
531
  this.activeTab === "chains"
445
532
  ? this.theme.fg("accent", this.theme.bold("[Chains]"))
446
533
  : this.theme.fg("muted", "Chains");
534
+ const stats =
535
+ this.activeTab === "stats"
536
+ ? this.theme.fg("accent", this.theme.bold("[Stats]"))
537
+ : this.theme.fg("muted", "Stats");
447
538
  const settings =
448
539
  this.activeTab === "settings"
449
540
  ? this.theme.fg("accent", this.theme.bold("[Settings]"))
450
541
  : this.theme.fg("muted", "Settings");
451
- const tabs = `${servers} ${chains} ${settings}`;
542
+ const tabs = `${servers} ${chains} ${stats} ${settings}`;
452
543
  const title = this.theme.fg("dim", this.theme.bold("CodeMCP"));
453
544
  const gap = " ".repeat(Math.max(1, width - visibleWidth(tabs) - visibleWidth(title)));
454
545
  return truncateToWidth(`${tabs}${gap}${title}`, width);
@@ -659,6 +750,57 @@ class ServerManagerModal implements Component, Focusable {
659
750
  return lines;
660
751
  }
661
752
 
753
+ private renderStats(width: number): string[] {
754
+ const stats = this.options.stats;
755
+ const lifetime = stats.lifetime;
756
+ const recent = stats.recent;
757
+ const cacheTotal = stats.cacheHits + stats.cacheMisses;
758
+ const cacheRate = cacheTotal > 0 ? `${Math.round((100 * stats.cacheHits) / cacheTotal)}%` : "—";
759
+ const lines = [
760
+ this.theme.fg("dim", this.theme.bold("LOCAL TELEMETRY")),
761
+ this.theme.fg(
762
+ "muted",
763
+ `Updated ${stats.updatedAt > 0 ? new Date(stats.updatedAt * 1_000).toLocaleString() : "never"} · bounded rollups`,
764
+ ),
765
+ "",
766
+ `${this.theme.fg("dim", "Lifetime")} ${lifetime.count.toLocaleString()} runs · ${lifetime.success.toLocaleString()} ok · ${lifetime.failure.toLocaleString()} failed`,
767
+ `${this.theme.fg("dim", "Calls")} ${lifetime.calls.toLocaleString()} MCP · ${lifetime.chainCalls.toLocaleString()} nested chains`,
768
+ `${this.theme.fg("dim", "Bytes")} ${formatBytes(lifetime.inputBytes)} in · ${formatBytes(lifetime.outputBytes)} out`,
769
+ `${this.theme.fg("dim", "Latency")} ${formatMilliseconds(lifetime.p50Ms)} p50 · ${formatMilliseconds(lifetime.p95Ms)} p95 · ${formatMilliseconds(lifetime.maxMs)} max`,
770
+ `${this.theme.fg("dim", "Results")} ${formatBytes(lifetime.p50OutputBytes)} p50 · ${formatBytes(lifetime.p95OutputBytes)} p95`,
771
+ `${this.theme.fg("dim", "Withheld")} ${formatBytes(Math.max(0, stats.upstreamOutputBytes - lifetime.outputBytes))} upstream bytes kept out of final results`,
772
+ `${this.theme.fg("dim", "Recent")} ${recent.count.toLocaleString()} runs in retained hourly buckets`,
773
+ `${this.theme.fg("dim", "Observed")} ${stats.serverCount} servers · ${stats.toolCount} tools · ${cacheRate} cache hit`,
774
+ "",
775
+ this.theme.fg("dim", this.theme.bold("OPERATIONS")),
776
+ ];
777
+ if (stats.operations.length === 0) lines.push(this.theme.fg("muted", "No operations yet"));
778
+ for (const operation of stats.operations) {
779
+ lines.push(
780
+ truncateToWidth(
781
+ `${operation.name.padEnd(18)} ${operation.rollup.count.toLocaleString().padStart(8)} · ${operation.rollup.failure.toLocaleString()} failed · ${formatMilliseconds(operation.rollup.averageMs)} avg`,
782
+ width,
783
+ ),
784
+ );
785
+ }
786
+ lines.push("", this.theme.fg("dim", this.theme.bold("FAILURE STAGES")));
787
+ if (stats.failures.length === 0) lines.push(this.theme.fg("muted", "No failures yet"));
788
+ for (const failure of stats.failures) {
789
+ lines.push(`${failure.stage.padEnd(18)} ${failure.count.toLocaleString().padStart(8)}`);
790
+ }
791
+ lines.push("", this.theme.fg("dim", this.theme.bold("PHASES")));
792
+ if (stats.phases.length === 0) lines.push(this.theme.fg("muted", "No phase timings yet"));
793
+ for (const phase of stats.phases) {
794
+ lines.push(
795
+ truncateToWidth(
796
+ `${phase.name.padEnd(18)} ${phase.count.toLocaleString().padStart(8)} · ${formatMilliseconds(phase.p50Ms)} p50 · ${formatMilliseconds(phase.p95Ms)} p95 · ${formatMilliseconds(phase.maxMs)} max`,
797
+ width,
798
+ ),
799
+ );
800
+ }
801
+ return lines.slice(0, Math.max(1, modalBodyRows()));
802
+ }
803
+
662
804
  private renderSettings(width: number): string[] {
663
805
  const splitHeight = Math.max(1, modalBodyRows());
664
806
  const leftWidth = Math.min(38, Math.max(28, Math.floor(width * 0.42)));
@@ -714,7 +856,10 @@ class ServerManagerModal implements Component, Focusable {
714
856
  return `tab servers · ↑/↓ navigate · ←/→/enter change · ctrl+s save · esc close${pending}`;
715
857
  }
716
858
  if (this.activeTab === "chains") {
717
- return `tab settings · ↑/↓ navigate · space toggle · r revalidate · del delete · esc close${pending}`;
859
+ return `tab stats · ↑/↓ navigate · space toggle · r revalidate · del delete · esc close${pending}`;
860
+ }
861
+ if (this.activeTab === "stats") {
862
+ return `tab settings · bounded local rollups · esc close${pending}`;
718
863
  }
719
864
  return `tab chains · ←/→ pane · ↑/↓ navigate · space toggle · d discover · esc close${pending}`;
720
865
  }
@@ -1021,6 +1166,105 @@ class ServerManagerModal implements Component, Focusable {
1021
1166
  }
1022
1167
  }
1023
1168
 
1169
+ function emptyStatsRollup(): StatsRollupState {
1170
+ return {
1171
+ count: 0,
1172
+ success: 0,
1173
+ failure: 0,
1174
+ inputBytes: 0,
1175
+ outputBytes: 0,
1176
+ calls: 0,
1177
+ chainCalls: 0,
1178
+ averageMs: 0,
1179
+ p50Ms: 0,
1180
+ p95Ms: 0,
1181
+ maxMs: 0,
1182
+ p50OutputBytes: 0,
1183
+ p95OutputBytes: 0,
1184
+ };
1185
+ }
1186
+
1187
+ function parseStatsRollup(value: unknown): StatsRollupState {
1188
+ if (!isRecord(value)) return emptyStatsRollup();
1189
+ const duration = isRecord(value.duration_ms) ? value.duration_ms : {};
1190
+ const outputSize = isRecord(value.output_size_bytes) ? value.output_size_bytes : {};
1191
+ return {
1192
+ count: numberField(value, "count"),
1193
+ success: numberField(value, "success"),
1194
+ failure: numberField(value, "failure"),
1195
+ inputBytes: numberField(value, "input_bytes"),
1196
+ outputBytes: numberField(value, "output_bytes"),
1197
+ calls: numberField(value, "calls"),
1198
+ chainCalls: numberField(value, "chain_calls"),
1199
+ averageMs: numberField(duration, "average"),
1200
+ p50Ms: histogramPercentile(duration, 0.5),
1201
+ p95Ms: histogramPercentile(duration, 0.95),
1202
+ maxMs: numberField(duration, "max"),
1203
+ p50OutputBytes: histogramPercentile(outputSize, 0.5),
1204
+ p95OutputBytes: histogramPercentile(outputSize, 0.95),
1205
+ };
1206
+ }
1207
+
1208
+ function addStatsRollups(left: StatsRollupState, right: StatsRollupState): StatsRollupState {
1209
+ const count = left.count + right.count;
1210
+ return {
1211
+ count,
1212
+ success: left.success + right.success,
1213
+ failure: left.failure + right.failure,
1214
+ inputBytes: left.inputBytes + right.inputBytes,
1215
+ outputBytes: left.outputBytes + right.outputBytes,
1216
+ calls: left.calls + right.calls,
1217
+ chainCalls: left.chainCalls + right.chainCalls,
1218
+ averageMs:
1219
+ count > 0 ? (left.averageMs * left.count + right.averageMs * right.count) / count : 0,
1220
+ p50Ms: Math.max(left.p50Ms, right.p50Ms),
1221
+ p95Ms: Math.max(left.p95Ms, right.p95Ms),
1222
+ maxMs: Math.max(left.maxMs, right.maxMs),
1223
+ p50OutputBytes: Math.max(left.p50OutputBytes, right.p50OutputBytes),
1224
+ p95OutputBytes: Math.max(left.p95OutputBytes, right.p95OutputBytes),
1225
+ };
1226
+ }
1227
+
1228
+ function parseNamedStatsRollups(value: unknown): Array<{ name: string; rollup: StatsRollupState }> {
1229
+ if (!isRecord(value)) return [];
1230
+ return Object.entries(value)
1231
+ .map(([name, item]) => ({ name, rollup: parseStatsRollup(item) }))
1232
+ .sort(
1233
+ (left, right) =>
1234
+ right.rollup.count - left.rollup.count || left.name.localeCompare(right.name),
1235
+ );
1236
+ }
1237
+
1238
+ function histogramPercentile(value: Record<string, unknown>, percentile: number): number {
1239
+ const count = numberField(value, "count");
1240
+ if (count === 0 || !Array.isArray(value.buckets)) return 0;
1241
+ const target = Math.ceil(count * percentile);
1242
+ let cumulative = 0;
1243
+ for (const bucket of value.buckets) {
1244
+ if (!isRecord(bucket)) continue;
1245
+ cumulative += numberField(bucket, "count");
1246
+ if (cumulative < target) continue;
1247
+ const boundary = bucket.le;
1248
+ return typeof boundary === "number" ? boundary : numberField(value, "max");
1249
+ }
1250
+ return numberField(value, "max");
1251
+ }
1252
+
1253
+ function numberField(value: Record<string, unknown>, key: string): number {
1254
+ const item = value[key];
1255
+ return typeof item === "number" && Number.isFinite(item) && item >= 0 ? item : 0;
1256
+ }
1257
+
1258
+ function formatBytes(value: number): string {
1259
+ if (value < 1_024) return `${value} B`;
1260
+ if (value < 1_024 * 1_024) return `${(value / 1_024).toFixed(1)} KiB`;
1261
+ return `${(value / (1_024 * 1_024)).toFixed(1)} MiB`;
1262
+ }
1263
+
1264
+ function formatMilliseconds(value: number): string {
1265
+ return value >= 1_000 ? `${(value / 1_000).toFixed(2)}s` : `${value.toFixed(1)}ms`;
1266
+ }
1267
+
1024
1268
  function parseTools(value: unknown): ToolModalState[] {
1025
1269
  if (!Array.isArray(value)) return [];
1026
1270
  return value.flatMap((tool) => {
package/src/output.ts CHANGED
@@ -1,22 +1,14 @@
1
- import {
2
- DEFAULT_MAX_BYTES,
3
- DEFAULT_MAX_LINES,
4
- formatSize,
5
- truncateHead,
6
- } from "@earendil-works/pi-coding-agent";
1
+ import { DEFAULT_MAX_BYTES, formatSize } from "@earendil-works/pi-coding-agent";
7
2
 
8
3
  export interface CodeMcpOutputDetails {
9
4
  truncated: boolean;
10
5
  outputBytes: number;
11
6
  totalBytes: number;
12
- outputLines: number;
13
- totalLines: number;
14
7
  outputTokens: number;
15
8
  }
16
9
 
17
10
  export interface CodeMcpOutputLimits {
18
11
  maxBytes?: number;
19
- maxLines?: number;
20
12
  }
21
13
 
22
14
  export function formatCodeMcpOutput(
@@ -26,27 +18,38 @@ export function formatCodeMcpOutput(
26
18
  text: string;
27
19
  details: CodeMcpOutputDetails;
28
20
  } {
29
- const serialized = JSON.stringify(value, null, 2);
30
- const truncation = truncateHead(serialized, {
31
- maxBytes: limits.maxBytes ?? DEFAULT_MAX_BYTES,
32
- maxLines: limits.maxLines ?? DEFAULT_MAX_LINES,
33
- });
34
- let text = truncation.content;
35
- if (truncation.truncated) {
21
+ const serialized = JSON.stringify(value) ?? "null";
22
+ const totalBytes = Buffer.byteLength(serialized);
23
+ const maxBytes = limits.maxBytes ?? DEFAULT_MAX_BYTES;
24
+ const truncated = totalBytes > maxBytes;
25
+ const content = truncated ? truncateUtf8(serialized, maxBytes) : serialized;
26
+ const outputBytes = Buffer.byteLength(content);
27
+ let text = content;
28
+ if (truncated) {
36
29
  text +=
37
- `\n\n[Output truncated: showing ${truncation.outputLines} of ` +
38
- `${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ` +
39
- `${formatSize(truncation.totalBytes)}). The full result was not persisted.]`;
30
+ `\n\n[Output truncated: showing ${formatSize(outputBytes)} of ` +
31
+ `${formatSize(totalBytes)}. The full result was not persisted.]`;
40
32
  }
41
33
  return {
42
34
  text,
43
35
  details: {
44
- truncated: truncation.truncated,
45
- outputBytes: truncation.outputBytes,
46
- totalBytes: truncation.totalBytes,
47
- outputLines: truncation.outputLines,
48
- totalLines: truncation.totalLines,
36
+ truncated,
37
+ outputBytes,
38
+ totalBytes,
49
39
  outputTokens: Math.ceil(text.length / 4),
50
40
  },
51
41
  };
52
42
  }
43
+
44
+ function truncateUtf8(value: string, maxBytes: number): string {
45
+ const encoded = Buffer.from(value);
46
+ const decoder = new TextDecoder("utf-8", { fatal: true });
47
+ for (let end = Math.min(maxBytes, encoded.length); end > 0; end -= 1) {
48
+ try {
49
+ return decoder.decode(encoded.subarray(0, end));
50
+ } catch {
51
+ // Back up to the previous complete UTF-8 code point.
52
+ }
53
+ }
54
+ return "";
55
+ }
package/src/prompts.ts ADDED
@@ -0,0 +1,24 @@
1
+ export const SEARCH_PROMPT_GUIDELINES = [
2
+ "Use search when the exact current SDK signature is missing; reuse a signature already present in context.",
3
+ "Use capability search for ranked discovery and inventory mode for enumeration; signatures search includes exact stubs for up to three top matches, so inspect only selected alternatives.",
4
+ ] as const;
5
+
6
+ export const INSPECT_PROMPT_GUIDELINES = [
7
+ "Batch-inspect selected alternatives from one search when their exact stubs are needed; do not repeat capability search for those same calls.",
8
+ ] as const;
9
+
10
+ export const EXECUTE_PROMPT_GUIDELINES = [
11
+ "Use programmatic execution for a bounded workflow when code can deterministically filter, join, aggregate, deduplicate, validate, or reduce intermediate results.",
12
+ "Keep a model turn between calls when an intermediate result changes the semantic decision or user approval is required.",
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.",
15
+ ] as const;
16
+
17
+ export const SAVE_CHAIN_PROMPT_GUIDELINES = [
18
+ "Save only after the user explicitly asks or accepts, and only after the same code has executed successfully.",
19
+ "Use project scope when available unless the user explicitly requests global scope; make schemas describe the exact parameterized contract.",
20
+ ] as const;
21
+
22
+ export const MANAGE_CHAIN_PROMPT_GUIDELINES = [
23
+ "List saved chains freely, but enable, disable, revalidate, or delete only after the user explicitly requests that mutation.",
24
+ ] as const;
package/src/settings.ts CHANGED
@@ -9,7 +9,6 @@ export interface CodeMcpSettings {
9
9
  maxCalls: number;
10
10
  resultLimitKiB: number;
11
11
  outputLimitKiB: number;
12
- outputLineLimit: number;
13
12
  disabledTools: Record<string, string[]>;
14
13
  }
15
14
 
@@ -24,7 +23,6 @@ export const DEFAULT_CODEMCP_SETTINGS: Readonly<CodeMcpSettings> = {
24
23
  maxCalls: 50,
25
24
  resultLimitKiB: 16,
26
25
  outputLimitKiB: 50,
27
- outputLineLimit: 2_000,
28
26
  disabledTools: {},
29
27
  };
30
28
 
@@ -37,36 +35,40 @@ const ALLOWED_KEYS = new Set([
37
35
  "maxCalls",
38
36
  "resultLimitKiB",
39
37
  "outputLimitKiB",
40
- "outputLineLimit",
41
38
  "disabledTools",
42
39
  ]);
43
40
 
44
41
  export function loadCodeMcpSettings(path: string): CodeMcpSettings {
45
42
  if (!existsSync(path)) return cloneDefaults();
46
43
  const root = readJsonObject(path, "CodeMCP settings");
47
- const unknown = Object.keys(root).filter((key) => !ALLOWED_KEYS.has(key));
44
+ const version = root.version ?? 1;
45
+ if (version !== 1 && version !== 2) {
46
+ throw new Error(`Unsupported CodeMCP settings version: ${String(version)}`);
47
+ }
48
+ const migrated =
49
+ version === 1
50
+ ? Object.fromEntries(Object.entries(root).filter(([key]) => key !== "outputLineLimit"))
51
+ : root;
52
+ const unknown = Object.keys(migrated).filter((key) => !ALLOWED_KEYS.has(key));
48
53
  if (unknown.length > 0) {
49
54
  throw new Error(`Unknown CodeMCP settings: ${unknown.join(", ")}`);
50
55
  }
51
- const version = root.version ?? 1;
52
- if (version !== 1) throw new Error(`Unsupported CodeMCP settings version: ${String(version)}`);
53
56
 
54
57
  return {
55
- backgroundWarmup: booleanSetting(root, "backgroundWarmup"),
56
- cacheTtlHours: integerSetting(root, "cacheTtlHours", 0, 720),
57
- executionTimeoutSeconds: integerSetting(root, "executionTimeoutSeconds", 1, 300),
58
- toolTimeoutSeconds: integerSetting(root, "toolTimeoutSeconds", 1, 300),
59
- maxCalls: integerSetting(root, "maxCalls", 1, 200),
60
- resultLimitKiB: integerSetting(root, "resultLimitKiB", 1, 1_024),
61
- outputLimitKiB: integerSetting(root, "outputLimitKiB", 1, 1_024),
62
- outputLineLimit: integerSetting(root, "outputLineLimit", 1, 10_000),
63
- disabledTools: disabledToolSetting(root.disabledTools),
58
+ backgroundWarmup: booleanSetting(migrated, "backgroundWarmup"),
59
+ cacheTtlHours: integerSetting(migrated, "cacheTtlHours", 0, 720),
60
+ executionTimeoutSeconds: integerSetting(migrated, "executionTimeoutSeconds", 1, 300),
61
+ toolTimeoutSeconds: integerSetting(migrated, "toolTimeoutSeconds", 1, 300),
62
+ maxCalls: integerSetting(migrated, "maxCalls", 1, 200),
63
+ resultLimitKiB: integerSetting(migrated, "resultLimitKiB", 1, 1_024),
64
+ outputLimitKiB: integerSetting(migrated, "outputLimitKiB", 1, 1_024),
65
+ disabledTools: disabledToolSetting(migrated.disabledTools),
64
66
  };
65
67
  }
66
68
 
67
69
  export function saveCodeMcpSettings(path: string, settings: CodeMcpSettings): void {
68
70
  writeJsonObjectAtomically(path, {
69
- version: 1,
71
+ version: 2,
70
72
  backgroundWarmup: settings.backgroundWarmup,
71
73
  cacheTtlHours: settings.cacheTtlHours,
72
74
  executionTimeoutSeconds: settings.executionTimeoutSeconds,
@@ -74,7 +76,6 @@ export function saveCodeMcpSettings(path: string, settings: CodeMcpSettings): vo
74
76
  maxCalls: settings.maxCalls,
75
77
  resultLimitKiB: settings.resultLimitKiB,
76
78
  outputLimitKiB: settings.outputLimitKiB,
77
- outputLineLimit: settings.outputLineLimit,
78
79
  disabledTools: settings.disabledTools,
79
80
  });
80
81
  }