@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 +1 -1
- package/extensions/README.md +1 -1
- package/extensions/serena-pool/index.ts +110 -0
- package/extensions/serena-pool/package.json +23 -0
- package/extensions/sp-terminal-overlay/index.ts +7 -3
- package/extensions/xtrm-ui/index.ts +16 -31
- package/package.json +1 -1
- package/src/extensions/serena-pool.ts +3 -0
- package/src/registry.ts +2 -0
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
|
|
50
|
+
- `sp-terminal-overlay` — `/sp-feed` streaming overlay, `/sp-ps` snapshot overlay, and `/xtrm-terminal` shell overlay for specialist monitoring.
|
package/extensions/README.md
CHANGED
|
@@ -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]`
|
|
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
|
|
30
|
-
|
|
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
|
|
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 =
|
|
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: [
|
|
348
|
-
gitnexus: [
|
|
349
|
-
structured: [
|
|
350
|
-
process: [
|
|
351
|
-
external: [
|
|
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
|
-
|
|
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+(
|
|
360
|
-
if (!match?.[1]) return line;
|
|
361
|
-
return externalToolBadgeColor(kind, match[
|
|
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
|
|
392
|
-
|
|
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))
|
|
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
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 },
|