@jaggerxtrm/pi-extensions 0.7.20 → 0.7.22

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
@@ -47,4 +47,4 @@ After install, keep `.pi/settings.json` package wiring pointed at `npm:@jaggerxt
47
47
  Notable bundled extensions include:
48
48
 
49
49
  - `xtrm-ui` — XTRM Pi chrome, native tool summaries, selectable external tool chrome (`/xtrm-ui chrome background|box`).
50
- - `sp-terminal-overlay` — `/sp-feed`, `/sp-ps`, and `/xtrm-terminal` streaming overlays for specialist monitoring.
50
+ - `sp-terminal-overlay` — `/sp-feed` streaming overlay, `/sp-ps` snapshot overlay, and `/xtrm-terminal` shell overlay for specialist monitoring.
@@ -11,7 +11,7 @@ Streaming terminal-style overlay for specialist/process monitoring commands.
11
11
  Commands:
12
12
 
13
13
  - `/sp-feed [args]` — opens `sp feed -f [args]` in an overlay.
14
- - `/sp-ps [args]` / `/xtrm-ps [args]` — opens `sp ps [args]` (defaults to `sp ps --follow`) in an overlay.
14
+ - `/sp-ps [args]` / `/xtrm-ps [args]` — opens a one-shot `sp ps [args]` snapshot in an overlay; `--follow`/`-f` are stripped to avoid repaint loops.
15
15
  - `/xtrm-terminal <command>` — opens an arbitrary shell command in an overlay.
16
16
 
17
17
  Keys: `Esc`/`q` close, `r` restart, arrows/page keys scroll.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * serena-pool — shared Serena daemon per repo root
3
+ *
4
+ * Sets SERENA_MCP_PORT to a deterministic port derived from the git root before
5
+ * pi-serena-tools session_start fires. If no Serena server is already listening
6
+ * on that port, spawns one (detached, persists after session ends).
7
+ *
8
+ * pi-serena-tools reads SERENA_MCP_PORT lazily in resolveSerenaPort(), finds the
9
+ * server already running, skips its own spawn, and stopServer() is a no-op since
10
+ * it never held serverProcess. No patches to pi-serena-tools needed.
11
+ */
12
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
+ import { spawn } from "node:child_process";
14
+ import { connect } from "node:net";
15
+ import { execSync } from "node:child_process";
16
+
17
+ const POOL_PORT_MIN = 40000;
18
+ const POOL_PORT_RANGE = 5000; // 40000–44999
19
+ const STARTUP_TIMEOUT_MS = 15_000;
20
+ const POLL_INTERVAL_MS = 300;
21
+
22
+ function hashToPort(s: string): number {
23
+ let h = 5381;
24
+ for (let i = 0; i < s.length; i++) {
25
+ h = ((h << 5) + h) ^ s.charCodeAt(i);
26
+ h = h >>> 0;
27
+ }
28
+ return POOL_PORT_MIN + (h % POOL_PORT_RANGE);
29
+ }
30
+
31
+ function getRepoRoot(cwd: string): string {
32
+ try {
33
+ return execSync("git rev-parse --show-toplevel", {
34
+ cwd,
35
+ encoding: "utf8",
36
+ stdio: ["ignore", "pipe", "ignore"],
37
+ }).trim();
38
+ } catch {
39
+ return cwd;
40
+ }
41
+ }
42
+
43
+ function isPortListening(port: number): Promise<boolean> {
44
+ return new Promise((resolve) => {
45
+ const socket = connect({ host: "127.0.0.1", port }, () => {
46
+ socket.destroy();
47
+ resolve(true);
48
+ });
49
+ socket.on("error", () => resolve(false));
50
+ socket.setTimeout(500, () => {
51
+ socket.destroy();
52
+ resolve(false);
53
+ });
54
+ });
55
+ }
56
+
57
+ async function waitForPort(port: number): Promise<boolean> {
58
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
59
+ while (Date.now() < deadline) {
60
+ if (await isPortListening(port)) return true;
61
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
62
+ }
63
+ return false;
64
+ }
65
+
66
+ async function ensureSerenaRunning(projectRoot: string, port: number): Promise<void> {
67
+ if (await isPortListening(port)) return;
68
+
69
+ const proc = spawn(
70
+ "uvx",
71
+ [
72
+ "--from", "git+https://github.com/oraios/serena",
73
+ "serena", "start-mcp-server",
74
+ "--transport", "streamable-http",
75
+ "--port", String(port),
76
+ "--project", projectRoot,
77
+ "--context", "agent",
78
+ ],
79
+ {
80
+ cwd: projectRoot,
81
+ env: { ...process.env },
82
+ stdio: "ignore",
83
+ detached: true,
84
+ },
85
+ );
86
+ proc.unref();
87
+
88
+ const ready = await waitForPort(port);
89
+ if (!ready) {
90
+ console.warn(`[serena-pool] Serena did not start on port ${port} within ${STARTUP_TIMEOUT_MS}ms`);
91
+ }
92
+ }
93
+
94
+ export default function registerSerenaPool(pi: ExtensionAPI) {
95
+ pi.on("session_start", async (_event: unknown, ctx: any) => {
96
+ const cwd: string = ctx?.cwd ?? process.cwd();
97
+ const projectRoot = getRepoRoot(cwd);
98
+ const port = hashToPort(projectRoot);
99
+
100
+ try {
101
+ await ensureSerenaRunning(projectRoot, port);
102
+ process.env.SERENA_MCP_PORT = String(port);
103
+ } catch (err) {
104
+ console.warn("[serena-pool] Error ensuring Serena running:", err);
105
+ }
106
+ });
107
+
108
+ // session_shutdown intentionally absent — server persists as daemon across sessions.
109
+ // Kill manually with: kill $(lsof -ti:PORT) or pkill -f "serena start-mcp-server"
110
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "serena-pool",
3
+ "version": "1.0.0",
4
+ "description": "Shared Serena daemon per repo root — sets SERENA_MCP_PORT before pi-serena-tools fires",
5
+ "keywords": [
6
+ "pi",
7
+ "pi-extension",
8
+ "serena",
9
+ "mcp",
10
+ "pool"
11
+ ],
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "main": "index.ts",
15
+ "peerDependencies": {
16
+ "@mariozechner/pi-coding-agent": "^0.56.0"
17
+ },
18
+ "pi": {
19
+ "extensions": [
20
+ "./index.ts"
21
+ ]
22
+ }
23
+ }
@@ -26,8 +26,12 @@ function resolveSpFeedCommand(args: string): string {
26
26
  }
27
27
 
28
28
  function resolveSpPsCommand(args: string): string {
29
- const trimmed = args.trim();
30
- return trimmed ? `sp ps ${trimmed}` : "sp ps --follow";
29
+ const snapshotArgs = args
30
+ .trim()
31
+ .split(/\s+/u)
32
+ .filter((part) => part && part !== "--follow" && part !== "-f")
33
+ .join(" ");
34
+ return snapshotArgs ? `sp ps ${snapshotArgs}` : "sp ps";
31
35
  }
32
36
 
33
37
  function openTerminalOverlay(ctx: ExtensionCommandContext, title: string, command: string): Promise<void> {
@@ -455,7 +459,7 @@ export default function spTerminalOverlayExtension(pi: ExtensionAPI): void {
455
459
  });
456
460
 
457
461
  pi.registerCommand("sp-ps", {
458
- description: "Open a streaming overlay for `sp ps --follow`",
462
+ description: "Open a snapshot overlay for `sp ps`",
459
463
  handler: async (args, ctx) => {
460
464
  await openTerminalOverlay(ctx, "sp ps", resolveSpPsCommand(args));
461
465
  },
@@ -252,7 +252,7 @@ type PatchableToolExecutionComponent = {
252
252
  type ExternalToolFrameKind = "serena" | "gitnexus" | "structured" | "process" | "external";
253
253
 
254
254
  const PATCHED_EXTERNAL_TOOL_FRAME = "__xtrmUiExternalToolFrame";
255
- const EXTERNAL_TOOL_FRAME_PATCH_VERSION = 10;
255
+ const EXTERNAL_TOOL_FRAME_PATCH_VERSION = 11;
256
256
  const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
257
257
 
258
258
  function stripAnsi(text: string): string {
@@ -326,39 +326,22 @@ function trimRenderedToolLines(lines: string[]): string[] {
326
326
  return lines.slice(start, end).map((line) => line.replace(/\s+$/u, ""));
327
327
  }
328
328
 
329
- function externalToolBgRgb(kind: ExternalToolFrameKind): [number, number, number] {
330
- const bgColors: Record<ExternalToolFrameKind, [number, number, number]> = {
331
- serena: [13, 34, 49],
332
- gitnexus: [31, 23, 55],
333
- structured: [35, 23, 55],
334
- process: [10, 42, 52],
335
- external: [27, 33, 43],
336
- };
337
- return bgColors[kind];
338
- }
339
-
340
- function externalToolBgColor(kind: ExternalToolFrameKind, text: string): string {
341
- const [r, g, b] = externalToolBgRgb(kind);
342
- return `\x1b[48;2;${r};${g};${b}m${text}\x1b[49m`;
343
- }
344
-
345
329
  function externalToolBadgeColor(kind: ExternalToolFrameKind, text: string): string {
346
330
  const bgColors: Record<ExternalToolFrameKind, [number, number, number]> = {
347
- serena: [26, 96, 132],
348
- gitnexus: [82, 58, 150],
349
- structured: [105, 61, 150],
350
- process: [17, 118, 145],
351
- external: [74, 88, 112],
331
+ serena: [82, 210, 255],
332
+ gitnexus: [178, 154, 255],
333
+ structured: [205, 166, 255],
334
+ process: [92, 226, 255],
335
+ external: [178, 190, 210],
352
336
  };
353
337
  const [badgeR, badgeG, badgeB] = bgColors[kind];
354
- const [rowR, rowG, rowB] = externalToolBgRgb(kind);
355
- return `\x1b[1m\x1b[48;2;${badgeR};${badgeG};${badgeB}m${text}\x1b[22m\x1b[48;2;${rowR};${rowG};${rowB}m`;
338
+ return `\x1b[38;2;3;8;12m\x1b[48;2;${badgeR};${badgeG};${badgeB}m${text}\x1b[39m\x1b[49m`;
356
339
  }
357
340
 
358
341
  function highlightExternalToolBadge(kind: ExternalToolFrameKind, line: string): string {
359
- const match = line.match(/^(•\s+(?:serena\s+\S+|gitnexus(?:_\S+)?|structured_return|process|\S+))/u);
360
- if (!match?.[1]) return line;
361
- return externalToolBadgeColor(kind, match[1]) + line.slice(match[1].length);
342
+ const match = line.match(/^(•\s+)(\S+)/u);
343
+ if (!match?.[1] || !match[2]) return line;
344
+ return match[1] + externalToolBadgeColor(kind, match[2]) + line.slice(match[1].length + match[2].length);
362
345
  }
363
346
 
364
347
  function externalToolBorderColor(kind: ExternalToolFrameKind, text: string): string {
@@ -388,9 +371,8 @@ function renderExternalToolBackgroundLines(
388
371
  const visibleLines = collapsedExternalToolLines(contentLines, expanded);
389
372
 
390
373
  return visibleLines.map((rawLine) => {
391
- const line = truncateToWidth(rawLine, Math.max(1, renderWidth - 2));
392
- const highlighted = highlightExternalToolBadge(kind, line);
393
- return externalToolBgColor(kind, ` ${padVisible(highlighted, Math.max(1, renderWidth - 2))} `);
374
+ const line = truncateToWidth(rawLine, Math.max(1, renderWidth));
375
+ return highlightExternalToolBadge(kind, line);
394
376
  });
395
377
  }
396
378
 
@@ -1421,7 +1403,10 @@ function registerXtrmUiTools(pi: ExtensionAPI, getPrefs: () => XtrmUiPrefs): voi
1421
1403
 
1422
1404
  pi.on("tool_result", async (event: ToolResultEvent, _ctx) => {
1423
1405
  if (event.isError) return undefined;
1424
- if (XTRM_BUILTIN_TOOLS.has(event.toolName)) return undefined;
1406
+ if (XTRM_BUILTIN_TOOLS.has(event.toolName)) {
1407
+ trackToolCallEnd(event.toolCallId);
1408
+ return undefined;
1409
+ }
1425
1410
  if (!getPrefs().compactExternalToolResults) return undefined;
1426
1411
 
1427
1412
  const text = getTextContent({ content: event.content as Array<{ type: string; text?: string }> });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaggerxtrm/pi-extensions",
3
- "version": "0.7.20",
3
+ "version": "0.7.22",
4
4
  "description": "Unified Pi extension entrypoint for xtrm-managed extensions",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -0,0 +1,3 @@
1
+ import registerExtension from "../../extensions/serena-pool/index.ts";
2
+
3
+ export default registerExtension;
package/src/registry.ts CHANGED
@@ -9,6 +9,7 @@ import customProviderQwenCliExtension from "./extensions/custom-provider-qwen-cl
9
9
  import gitCheckpointExtension from "./extensions/git-checkpoint.ts";
10
10
  import lspBootstrapExtension from "./extensions/lsp-bootstrap.ts";
11
11
  import piSerenaCompactExtension from "./extensions/pi-serena-compact.ts";
12
+ import serenaPoolExtension from "./extensions/serena-pool.ts";
12
13
  import qualityGatesExtension from "./extensions/quality-gates.ts";
13
14
  import serviceSkillsExtension from "./extensions/service-skills.ts";
14
15
  import sessionFlowExtension from "./extensions/session-flow.ts";
@@ -29,6 +30,7 @@ export const managedPiExtensions: readonly ManagedPiExtension[] = [
29
30
  { id: "custom-footer", register: customFooterExtension },
30
31
  { id: "custom-provider-qwen-cli", register: customProviderQwenCliExtension },
31
32
  { id: "git-checkpoint", register: gitCheckpointExtension },
33
+ { id: "serena-pool", register: serenaPoolExtension },
32
34
  { id: "lsp-bootstrap", register: lspBootstrapExtension },
33
35
  { id: "pi-serena-compact", register: piSerenaCompactExtension },
34
36
  { id: "quality-gates", register: qualityGatesExtension },