xtrm-tools 2.4.4 → 2.4.6
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/cli/package.json +1 -1
- package/config/hooks.json +0 -15
- package/config/pi/extensions/minimal-mode.ts +201 -0
- package/config/pi/settings.json.template +1 -1
- package/hooks/branch-state.mjs +16 -29
- package/package.json +1 -1
package/cli/package.json
CHANGED
package/config/hooks.json
CHANGED
|
@@ -16,16 +16,6 @@
|
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
18
|
"PreToolUse": [
|
|
19
|
-
{
|
|
20
|
-
"matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
21
|
-
"script": "main-guard.mjs",
|
|
22
|
-
"timeout": 5000
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"matcher": "Bash",
|
|
26
|
-
"script": "main-guard.mjs",
|
|
27
|
-
"timeout": 5000
|
|
28
|
-
},
|
|
29
19
|
{
|
|
30
20
|
"matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
31
21
|
"script": "beads-edit-gate.mjs",
|
|
@@ -43,11 +33,6 @@
|
|
|
43
33
|
"script": "beads-claim-sync.mjs",
|
|
44
34
|
"timeout": 5000
|
|
45
35
|
},
|
|
46
|
-
{
|
|
47
|
-
"matcher": "Bash",
|
|
48
|
-
"script": "main-guard-post-push.mjs",
|
|
49
|
-
"timeout": 5000
|
|
50
|
-
},
|
|
51
36
|
{
|
|
52
37
|
"matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
53
38
|
"script": "quality-check.cjs",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
createBashTool,
|
|
4
|
+
createEditTool,
|
|
5
|
+
createFindTool,
|
|
6
|
+
createGrepTool,
|
|
7
|
+
createLsTool,
|
|
8
|
+
createReadTool,
|
|
9
|
+
createWriteTool,
|
|
10
|
+
} from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
12
|
+
|
|
13
|
+
function getTextContent(result: any): string {
|
|
14
|
+
if (!result?.content || !Array.isArray(result.content)) return "";
|
|
15
|
+
return result.content
|
|
16
|
+
.filter((c: any) => c?.type === "text" && typeof c.text === "string")
|
|
17
|
+
.map((c: any) => c.text)
|
|
18
|
+
.join("\n")
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function oneLine(s: string): string {
|
|
23
|
+
return (s || "").replace(/\s+/g, " ").trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function summarize(result: any): { text: string; isError: boolean } {
|
|
27
|
+
const raw = getTextContent(result);
|
|
28
|
+
if (!raw) return { text: "done", isError: false };
|
|
29
|
+
const line = oneLine(raw.split("\n").find((l) => l.trim()) || "");
|
|
30
|
+
const lower = line.toLowerCase();
|
|
31
|
+
const isError = lower.includes("error") || lower.includes("failed") || lower.includes("exception");
|
|
32
|
+
return { text: line.slice(0, 140) || "done", isError };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
|
|
36
|
+
function createBuiltInTools(cwd: string) {
|
|
37
|
+
return {
|
|
38
|
+
read: createReadTool(cwd),
|
|
39
|
+
bash: createBashTool(cwd),
|
|
40
|
+
edit: createEditTool(cwd),
|
|
41
|
+
write: createWriteTool(cwd),
|
|
42
|
+
find: createFindTool(cwd),
|
|
43
|
+
grep: createGrepTool(cwd),
|
|
44
|
+
ls: createLsTool(cwd),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function getBuiltInTools(cwd: string) {
|
|
48
|
+
let tools = toolCache.get(cwd);
|
|
49
|
+
if (!tools) {
|
|
50
|
+
tools = createBuiltInTools(cwd);
|
|
51
|
+
toolCache.set(cwd, tools);
|
|
52
|
+
}
|
|
53
|
+
return tools;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function (pi: ExtensionAPI) {
|
|
57
|
+
let minimalEnabled = true;
|
|
58
|
+
let thinkingStatusEnabled = true;
|
|
59
|
+
let spinnerTimer: NodeJS.Timeout | null = null;
|
|
60
|
+
let spinnerIndex = 0;
|
|
61
|
+
const frames = ["thinking ", "thinking. ", "thinking.. ", "thinking..."];
|
|
62
|
+
|
|
63
|
+
const clearSpinner = (ctx: any) => {
|
|
64
|
+
if (spinnerTimer) {
|
|
65
|
+
clearInterval(spinnerTimer);
|
|
66
|
+
spinnerTimer = null;
|
|
67
|
+
}
|
|
68
|
+
if (ctx?.hasUI) {
|
|
69
|
+
ctx.ui.setStatus("thinking", undefined);
|
|
70
|
+
ctx.ui.setHeader(undefined);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const mountThinkingHeader = (ctx: any) => {
|
|
75
|
+
if (!ctx?.hasUI) return;
|
|
76
|
+
ctx.ui.setHeader((_tui: any, theme: any) => ({
|
|
77
|
+
invalidate() {},
|
|
78
|
+
render(width: number): string[] {
|
|
79
|
+
const text = frames[spinnerIndex];
|
|
80
|
+
return [oneLine(theme.fg("accent", text)).slice(0, width)];
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const startSpinner = (ctx: any) => {
|
|
86
|
+
if (!thinkingStatusEnabled || !ctx?.hasUI) return;
|
|
87
|
+
clearSpinner(ctx);
|
|
88
|
+
spinnerIndex = 0;
|
|
89
|
+
ctx.ui.setStatus("thinking", frames[spinnerIndex]);
|
|
90
|
+
mountThinkingHeader(ctx);
|
|
91
|
+
spinnerTimer = setInterval(() => {
|
|
92
|
+
spinnerIndex = (spinnerIndex + 1) % frames.length;
|
|
93
|
+
ctx.ui.setStatus("thinking", frames[spinnerIndex]);
|
|
94
|
+
mountThinkingHeader(ctx);
|
|
95
|
+
}, 220);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const renderCollapsedResult = (result: any, theme: any) => {
|
|
99
|
+
const s = summarize(result);
|
|
100
|
+
const icon = s.isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
101
|
+
const text = s.isError ? theme.fg("error", s.text) : theme.fg("muted", s.text);
|
|
102
|
+
if (minimalEnabled) return new Text(` ${icon} ${text}`, 0, 0);
|
|
103
|
+
return new Text(theme.fg("muted", ` → ${s.text}`), 0, 0);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const renderExpandedResult = (result: any, theme: any) => {
|
|
107
|
+
const text = getTextContent(result);
|
|
108
|
+
if (!text) return new Text("", 0, 0);
|
|
109
|
+
const output = text.split("\n").map((line) => theme.fg("toolOutput", line)).join("\n");
|
|
110
|
+
return new Text(`\n${output}`, 0, 0);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
pi.registerTool({
|
|
114
|
+
name: "bash",
|
|
115
|
+
label: "bash",
|
|
116
|
+
description: getBuiltInTools(process.cwd()).bash.description,
|
|
117
|
+
parameters: getBuiltInTools(process.cwd()).bash.parameters,
|
|
118
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
119
|
+
return getBuiltInTools(ctx.cwd).bash.execute(toolCallId, params, signal, onUpdate);
|
|
120
|
+
},
|
|
121
|
+
renderCall(args, theme) {
|
|
122
|
+
const cmd = oneLine(args.command || "");
|
|
123
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("bash"))}(${theme.fg("accent", cmd || "...")})`, 0, 0);
|
|
124
|
+
},
|
|
125
|
+
renderResult(result, { expanded }, theme) {
|
|
126
|
+
return expanded ? renderExpandedResult(result, theme) : renderCollapsedResult(result, theme);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
for (const name of ["read", "write", "edit", "find", "grep", "ls"] as const) {
|
|
131
|
+
pi.registerTool({
|
|
132
|
+
name,
|
|
133
|
+
label: name,
|
|
134
|
+
description: (getBuiltInTools(process.cwd()) as any)[name].description,
|
|
135
|
+
parameters: (getBuiltInTools(process.cwd()) as any)[name].parameters,
|
|
136
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
137
|
+
return (getBuiltInTools(ctx.cwd) as any)[name].execute(toolCallId, params, signal, onUpdate);
|
|
138
|
+
},
|
|
139
|
+
renderCall(args, theme) {
|
|
140
|
+
const suffix = oneLine(args.path || args.pattern || "");
|
|
141
|
+
return new Text(`${theme.fg("toolTitle", theme.bold(name))}${suffix ? `(${theme.fg("accent", suffix)})` : ""}`, 0, 0);
|
|
142
|
+
},
|
|
143
|
+
renderResult(result, { expanded }, theme) {
|
|
144
|
+
return expanded ? renderExpandedResult(result, theme) : renderCollapsedResult(result, theme);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pi.registerCommand("minimal-on", {
|
|
150
|
+
description: "Enable minimal collapsed tool output",
|
|
151
|
+
handler: async (_args, ctx) => {
|
|
152
|
+
minimalEnabled = true;
|
|
153
|
+
ctx.ui.notify("Minimal mode enabled", "info");
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
pi.registerCommand("minimal-off", {
|
|
158
|
+
description: "Disable minimal collapsed tool output",
|
|
159
|
+
handler: async (_args, ctx) => {
|
|
160
|
+
minimalEnabled = false;
|
|
161
|
+
ctx.ui.notify("Minimal mode disabled", "info");
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
pi.registerCommand("minimal-toggle", {
|
|
166
|
+
description: "Toggle minimal collapsed tool output",
|
|
167
|
+
handler: async (_args, ctx) => {
|
|
168
|
+
minimalEnabled = !minimalEnabled;
|
|
169
|
+
ctx.ui.notify(`Minimal mode ${minimalEnabled ? "enabled" : "disabled"}`, "info");
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
pi.registerCommand("thinking-status-toggle", {
|
|
174
|
+
description: "Toggle flashing thinking status indicator",
|
|
175
|
+
handler: async (_args, ctx) => {
|
|
176
|
+
thinkingStatusEnabled = !thinkingStatusEnabled;
|
|
177
|
+
if (!thinkingStatusEnabled) clearSpinner(ctx);
|
|
178
|
+
ctx.ui.notify(`Thinking status ${thinkingStatusEnabled ? "enabled" : "disabled"}`, "info");
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
183
|
+
startSpinner(ctx);
|
|
184
|
+
return undefined;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
188
|
+
clearSpinner(ctx);
|
|
189
|
+
return undefined;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
193
|
+
clearSpinner(ctx);
|
|
194
|
+
return undefined;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
198
|
+
clearSpinner(ctx);
|
|
199
|
+
return undefined;
|
|
200
|
+
});
|
|
201
|
+
}
|
package/hooks/branch-state.mjs
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
// Output: { hookSpecificOutput: { additionalSystemPrompt } }
|
|
6
6
|
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
|
-
import { readFileSync
|
|
9
|
-
import { join } from 'node:path';
|
|
10
|
-
import { resolveSessionId } from './beads-gate-utils.mjs';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
11
9
|
|
|
12
10
|
function readInput() {
|
|
13
11
|
try { return JSON.parse(readFileSync(0, 'utf-8')); } catch { return null; }
|
|
@@ -17,36 +15,25 @@ function getBranch(cwd) {
|
|
|
17
15
|
try {
|
|
18
16
|
return execSync('git branch --show-current', {
|
|
19
17
|
encoding: 'utf8', cwd,
|
|
20
|
-
stdio: ['pipe', 'pipe', 'pipe'], timeout:
|
|
18
|
+
stdio: ['pipe', 'pipe', 'pipe'], timeout: 2000,
|
|
21
19
|
}).trim() || null;
|
|
22
20
|
} catch { return null; }
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
encoding: 'utf8', cwd,
|
|
29
|
-
stdio: ['pipe', 'pipe', 'pipe'], timeout: 3000,
|
|
30
|
-
}).trim();
|
|
31
|
-
return out || null;
|
|
32
|
-
} catch { return null; }
|
|
33
|
-
}
|
|
23
|
+
try {
|
|
24
|
+
const input = readInput();
|
|
25
|
+
if (!input) process.exit(0);
|
|
34
26
|
|
|
35
|
-
const
|
|
36
|
-
|
|
27
|
+
const cwd = input.cwd || process.cwd();
|
|
28
|
+
const branch = getBranch(cwd);
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
const sessionId = resolveSessionId(input);
|
|
40
|
-
const branch = getBranch(cwd);
|
|
41
|
-
const isBeads = existsSync(join(cwd, '.beads'));
|
|
42
|
-
const claim = isBeads && sessionId ? getSessionClaim(sessionId, cwd) : null;
|
|
30
|
+
if (!branch) process.exit(0);
|
|
43
31
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
process.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
process.exit(0);
|
|
32
|
+
process.stdout.write(JSON.stringify({
|
|
33
|
+
hookSpecificOutput: { additionalSystemPrompt: `[Context: branch=${branch}]` },
|
|
34
|
+
}));
|
|
35
|
+
process.stdout.write('\n');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
} catch {
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|