xtrm-tools 0.7.4 → 0.7.7

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.
Files changed (46) hide show
  1. package/.xtrm/config/hooks.json +3 -0
  2. package/.xtrm/ext-src/auto-session-name/index.ts +29 -0
  3. package/.xtrm/ext-src/auto-session-name/package.json +16 -0
  4. package/.xtrm/ext-src/auto-update/index.ts +71 -0
  5. package/.xtrm/ext-src/auto-update/package.json +16 -0
  6. package/.xtrm/ext-src/beads/index.ts +232 -0
  7. package/.xtrm/ext-src/beads/package.json +19 -0
  8. package/.xtrm/ext-src/compact-header/index.ts +69 -0
  9. package/.xtrm/ext-src/compact-header/package.json +16 -0
  10. package/.xtrm/ext-src/core/adapter.ts +52 -0
  11. package/.xtrm/ext-src/core/guard-rules.ts +100 -0
  12. package/.xtrm/ext-src/core/lib.ts +3 -0
  13. package/.xtrm/ext-src/core/logger.ts +45 -0
  14. package/.xtrm/ext-src/core/package.json +18 -0
  15. package/.xtrm/ext-src/core/runner.ts +71 -0
  16. package/.xtrm/ext-src/core/session-state.ts +59 -0
  17. package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.combined.log +7 -0
  18. package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.stderr.log +0 -0
  19. package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.stdout.log +7 -0
  20. package/.xtrm/ext-src/custom-footer/index.ts +398 -0
  21. package/.xtrm/ext-src/custom-footer/package.json +19 -0
  22. package/.xtrm/ext-src/custom-provider-qwen-cli/index.ts +363 -0
  23. package/.xtrm/ext-src/custom-provider-qwen-cli/package.json +1 -0
  24. package/.xtrm/ext-src/git-checkpoint/index.ts +53 -0
  25. package/.xtrm/ext-src/git-checkpoint/package.json +16 -0
  26. package/.xtrm/ext-src/lsp-bootstrap/index.ts +134 -0
  27. package/.xtrm/ext-src/lsp-bootstrap/package.json +17 -0
  28. package/.xtrm/ext-src/pi-serena-compact/index.ts +121 -0
  29. package/.xtrm/ext-src/pi-serena-compact/package.json +16 -0
  30. package/.xtrm/ext-src/quality-gates/index.ts +66 -0
  31. package/.xtrm/ext-src/quality-gates/package.json +19 -0
  32. package/.xtrm/ext-src/service-skills/index.ts +108 -0
  33. package/.xtrm/ext-src/service-skills/package.json +19 -0
  34. package/.xtrm/ext-src/session-flow/index.ts +96 -0
  35. package/.xtrm/ext-src/session-flow/package.json +19 -0
  36. package/.xtrm/ext-src/xtrm-loader/index.ts +152 -0
  37. package/.xtrm/ext-src/xtrm-loader/package.json +19 -0
  38. package/.xtrm/ext-src/xtrm-ui/format.ts +282 -0
  39. package/.xtrm/ext-src/xtrm-ui/index.ts +1112 -0
  40. package/.xtrm/ext-src/xtrm-ui/package.json +21 -0
  41. package/.xtrm/ext-src/xtrm-ui/themes/pidex-dark.json +85 -0
  42. package/.xtrm/ext-src/xtrm-ui/themes/pidex-light.json +85 -0
  43. package/cli/dist/index.cjs +64 -2
  44. package/cli/dist/index.cjs.map +1 -1
  45. package/cli/package.json +1 -1
  46. package/package.json +2 -1
@@ -125,5 +125,8 @@
125
125
  ]
126
126
  }
127
127
  ]
128
+ },
129
+ "statusLine": {
130
+ "script": "statusline.mjs"
128
131
  }
129
132
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * oh-pi Auto Session Name Extension
3
+ *
4
+ * Automatically names sessions based on the first user message.
5
+ */
6
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+
8
+ export default function (pi: ExtensionAPI) {
9
+ let named = false;
10
+
11
+ pi.on("session_start", async (_event, ctx) => {
12
+ named = !!pi.getSessionName();
13
+ });
14
+
15
+ pi.on("agent_end", async (event) => {
16
+ if (named) return;
17
+ const userMsg = event.messages.find((m) => m.role === "user");
18
+ if (!userMsg) return;
19
+ const text = typeof userMsg.content === "string"
20
+ ? userMsg.content
21
+ : userMsg.content.filter((b) => b.type === "text").map((b) => (b as { text: string }).text).join(" ");
22
+ if (!text) return;
23
+ const name = text.slice(0, 60).replace(/\n/g, " ").trim();
24
+ if (name) {
25
+ pi.setSessionName(name);
26
+ named = true;
27
+ }
28
+ });
29
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@xtrm/pi-auto-session-name",
3
+ "version": "1.0.0",
4
+ "description": "xtrm Pi extension: auto-session-name",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "keywords": [
10
+ "pi",
11
+ "extension",
12
+ "xtrm"
13
+ ],
14
+ "author": "xtrm",
15
+ "license": "MIT"
16
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * oh-pi Auto Update — check for new oh-pi version on session start
3
+ */
4
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import { execSync } from "node:child_process";
6
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+
10
+ const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24h
11
+ const STAMP_FILE = join(homedir(), ".pi", "agent", ".update-check");
12
+
13
+ function readStamp(): number {
14
+ try { return Number(readFileSync(STAMP_FILE, "utf8").trim()) || 0; } catch { return 0; }
15
+ }
16
+
17
+ function writeStamp() {
18
+ try { writeFileSync(STAMP_FILE, String(Date.now())); } catch {}
19
+ }
20
+
21
+ function getLatestVersion(): string | null {
22
+ try {
23
+ return execSync("npm view oh-pi version", { encoding: "utf8", timeout: 8000 }).trim();
24
+ } catch { return null; }
25
+ }
26
+
27
+ function getCurrentVersion(): string | null {
28
+ // Read from the installed package.json
29
+ try {
30
+ const pkgPath = join(__dirname, "..", "..", "package.json");
31
+ if (existsSync(pkgPath)) {
32
+ return JSON.parse(readFileSync(pkgPath, "utf8")).version;
33
+ }
34
+ } catch {}
35
+ // Fallback: npm list
36
+ try {
37
+ const out = JSON.parse(execSync("npm list -g oh-pi --json --depth=0", { encoding: "utf8", timeout: 8000 }));
38
+ return out.dependencies?.["oh-pi"]?.version ?? null;
39
+ } catch { return null; }
40
+ }
41
+
42
+ export function isNewer(latest: string, current: string): boolean {
43
+ const a = latest.split(".").map(Number);
44
+ const b = current.split(".").map(Number);
45
+ for (let i = 0; i < 3; i++) {
46
+ if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
47
+ if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ export default function (pi: ExtensionAPI) {
53
+ pi.on("session_start", async (_event, ctx) => {
54
+ // Non-blocking: run check in background
55
+ setTimeout(async () => {
56
+ try {
57
+ if (Date.now() - readStamp() < CHECK_INTERVAL) return;
58
+ writeStamp();
59
+
60
+ const current = getCurrentVersion();
61
+ const latest = getLatestVersion();
62
+ if (!current || !latest || !isNewer(latest, current)) return;
63
+
64
+ const msg = `oh-pi ${latest} available (current: ${current}). Run: npx oh-pi@latest`;
65
+ if (ctx.hasUI) {
66
+ ctx.ui.toast?.(msg) ?? console.log(`\n💡 ${msg}\n`);
67
+ }
68
+ } catch {}
69
+ }, 2000);
70
+ });
71
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@xtrm/pi-auto-update",
3
+ "version": "1.0.0",
4
+ "description": "xtrm Pi extension: auto-update",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "keywords": [
10
+ "pi",
11
+ "extension",
12
+ "xtrm"
13
+ ],
14
+ "author": "xtrm",
15
+ "license": "MIT"
16
+ }
@@ -0,0 +1,232 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
3
+ import { SubprocessRunner, EventAdapter } from "@xtrm/pi-core";
4
+
5
+ export default function (pi: ExtensionAPI) {
6
+ const getCwd = (ctx: any) => ctx.cwd || process.cwd();
7
+
8
+ let cachedSessionId: string | null = null;
9
+ let memoryGateFired = false;
10
+
11
+ // Resolve a stable session ID across event types.
12
+ const getSessionId = (ctx: any): string => {
13
+ const fromManager = ctx?.sessionManager?.getSessionId?.();
14
+ const fromContext = ctx?.sessionId ?? ctx?.session_id;
15
+ const resolved = fromManager || fromContext || cachedSessionId || process.pid.toString();
16
+ if (resolved && !cachedSessionId) cachedSessionId = resolved;
17
+ return resolved;
18
+ };
19
+
20
+ const getSessionClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
21
+ const result = await SubprocessRunner.run("bd", ["kv", "get", `claimed:${sessionId}`], { cwd });
22
+ if (result.code !== 0) return null;
23
+ const claim = result.stdout.trim();
24
+ return claim.length > 0 ? claim : null;
25
+ };
26
+
27
+ const clearClaimMarker = async (sessionId: string, cwd: string) => {
28
+ await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
29
+ };
30
+
31
+ const isIssueInProgress = async (issueId: string, cwd: string): Promise<boolean | null> => {
32
+ const result = await SubprocessRunner.run("bd", ["show", issueId, "--json"], { cwd });
33
+ if (result.code !== 0 || !result.stdout.trim()) return null;
34
+ try {
35
+ const parsed = JSON.parse(result.stdout);
36
+ const issue = Array.isArray(parsed) ? parsed[0] : parsed;
37
+ if (!issue?.status) return null;
38
+ return issue.status === "in_progress";
39
+ } catch {
40
+ return null;
41
+ }
42
+ };
43
+
44
+ const getActiveClaim = async (sessionId: string, cwd: string): Promise<string | null> => {
45
+ const claim = await getSessionClaim(sessionId, cwd);
46
+ if (!claim) return null;
47
+
48
+ const inProgress = await isIssueInProgress(claim, cwd);
49
+ if (inProgress === false) {
50
+ await clearClaimMarker(sessionId, cwd);
51
+ return null;
52
+ }
53
+
54
+ return claim;
55
+ };
56
+
57
+ const getClosedThisSession = async (sessionId: string, cwd: string): Promise<string | null> => {
58
+ const result = await SubprocessRunner.run("bd", ["kv", "get", `closed-this-session:${sessionId}`], { cwd });
59
+ if (result.code !== 0) return null;
60
+ const issue = result.stdout.trim();
61
+ return issue.length > 0 ? issue : null;
62
+ };
63
+
64
+ const clearSessionMarkers = async (sessionId: string, cwd: string) => {
65
+ await SubprocessRunner.run("bd", ["kv", "clear", `claimed:${sessionId}`], { cwd });
66
+ await SubprocessRunner.run("bd", ["kv", "clear", `closed-this-session:${sessionId}`], { cwd });
67
+ };
68
+
69
+ const hasTrackableWork = async (cwd: string): Promise<boolean> => {
70
+ const result = await SubprocessRunner.run("bd", ["list"], { cwd });
71
+ if (result.code === 0) {
72
+ const counts = EventAdapter.parseBdCounts(result.stdout);
73
+ if (counts) return (counts.open + counts.inProgress) > 0;
74
+ }
75
+ return false;
76
+ };
77
+
78
+ const stripQuoted = (command: string): string => command.replace(/'[^']*'|"[^"]*"/g, "");
79
+ const isSpecialistsSubprocessCommand = (commandUnquoted: string): boolean =>
80
+ /\bspecialists\s+(run|resume|result|feed|stop|status)\b/.test(commandUnquoted);
81
+
82
+ const getClosedIssueIdFromCommand = (commandUnquoted: string): string | null => {
83
+ const match = commandUnquoted.match(/\bbd\s+close\s+(\S+)/);
84
+ const issueId = match?.[1]?.trim();
85
+ if (!issueId || issueId.startsWith("-")) return null;
86
+ return issueId;
87
+ };
88
+
89
+ const hasIssueMemoryAck = async (issueId: string, cwd: string): Promise<boolean> => {
90
+ const result = await SubprocessRunner.run("bd", ["kv", "get", `memory-acked:${issueId}`], { cwd });
91
+ return result.code === 0 && result.stdout.trim().length > 0;
92
+ };
93
+
94
+ const closeMemoryBlockReason = (issueId: string): string =>
95
+ `MEMORY_GATE_BLOCK issue=${issueId} run="bd remember '<insight>' && bd kv set 'memory-acked:${issueId}' 'saved:<key>'" or="bd kv set 'memory-acked:${issueId}' 'nothing novel:<reason>'" then="bd close ${issueId} --reason='<reason>'"`;
96
+
97
+ pi.on("session_start", async (_event, ctx) => {
98
+ cachedSessionId = ctx?.sessionManager?.getSessionId?.() ?? ctx?.sessionId ?? ctx?.session_id ?? cachedSessionId;
99
+ return undefined;
100
+ });
101
+
102
+ pi.on("tool_call", async (event, ctx) => {
103
+ const cwd = getCwd(ctx);
104
+ if (!EventAdapter.isBeadsProject(cwd)) return undefined;
105
+ const sessionId = getSessionId(ctx);
106
+
107
+ if (EventAdapter.isMutatingFileTool(event)) {
108
+ const claim = await getActiveClaim(sessionId, cwd);
109
+ if (!claim) {
110
+ const hasWork = await hasTrackableWork(cwd);
111
+ if (hasWork) {
112
+ if (ctx.hasUI) {
113
+ ctx.ui.notify("Beads: Edit blocked. Claim an issue first.", "warning");
114
+ }
115
+ return {
116
+ block: true,
117
+ reason: `No active claim for session ${sessionId}.\n bd update <id> --claim\n`,
118
+ };
119
+ }
120
+ }
121
+ }
122
+
123
+ if (isToolCallEventType("bash", event)) {
124
+ const command = event.input.command ?? "";
125
+ const commandUnquoted = stripQuoted(command);
126
+
127
+ if (isSpecialistsSubprocessCommand(commandUnquoted)) return undefined;
128
+
129
+ const closedIssueId = getClosedIssueIdFromCommand(commandUnquoted);
130
+ if (closedIssueId) {
131
+ const acked = await hasIssueMemoryAck(closedIssueId, cwd);
132
+ if (!acked) {
133
+ return {
134
+ block: true,
135
+ reason: closeMemoryBlockReason(closedIssueId),
136
+ };
137
+ }
138
+ }
139
+
140
+ if (/\bgit\s+commit\b/.test(commandUnquoted)) {
141
+ const claim = await getActiveClaim(sessionId, cwd);
142
+ if (claim) {
143
+ return {
144
+ block: true,
145
+ reason: `Active claim [${claim}] — close it first.\n bd close ${claim}\n (Pi workflow) publish/merge are external steps; do not rely on xtrm finish.\n`,
146
+ };
147
+ }
148
+ }
149
+ }
150
+
151
+ return undefined;
152
+ });
153
+
154
+ pi.on("tool_result", async (event, ctx) => {
155
+ if (!isBashToolResult(event)) return undefined;
156
+
157
+ const command = event.input.command || "";
158
+ const sessionId = getSessionId(ctx);
159
+ const cwd = getCwd(ctx);
160
+
161
+ // Auto-claim on bd update --claim regardless of exit code.
162
+ if (/\bbd\s+update\b/.test(command) && /--claim\b/.test(command)) {
163
+ const issueMatch = command.match(/\bbd\s+update\s+(\S+)/);
164
+ if (issueMatch) {
165
+ const issueId = issueMatch[1];
166
+ await SubprocessRunner.run("bd", ["kv", "set", `claimed:${sessionId}`, issueId], { cwd });
167
+ memoryGateFired = false;
168
+ const claimNotice = `\n\n✅ **Beads**: Session \`${sessionId}\` claimed issue \`${issueId}\`. File edits are now unblocked.`;
169
+ return { content: [...event.content, { type: "text", text: claimNotice }] };
170
+ }
171
+ }
172
+
173
+ if (/\bbd\s+close\b/.test(command) && !event.isError) {
174
+ const closeMatch = command.match(/\bbd\s+close\s+(\S+)/);
175
+ const closedIssueId = closeMatch?.[1] ?? null;
176
+
177
+ if (closedIssueId) {
178
+ await SubprocessRunner.run("bd", ["kv", "set", `closed-this-session:${sessionId}`, closedIssueId], { cwd });
179
+ memoryGateFired = false;
180
+ }
181
+
182
+ const memoryGateText = closedIssueId
183
+ ? `\n\n**Beads Memory Gate**: close-time memory ack verified for \`${closedIssueId}\` (\`memory-acked:${closedIssueId}\`).`
184
+ : `\n\n**Beads**: Work completed. Consider if this session produced insights worth persisting via \`bd remember\`.`;
185
+ return { content: [...event.content, { type: "text", text: memoryGateText }] };
186
+ }
187
+
188
+ return undefined;
189
+ });
190
+
191
+ // Memory gate: clean up session markers and check ack at agent_end/session_shutdown.
192
+ // Memory gate prompt was already injected into bd close tool_result context (silent, agent-visible only).
193
+ // No UI notification — parity with Claude Stop hook {additionalContext} pattern.
194
+ const triggerMemoryGateIfNeeded = async (ctx: any) => {
195
+ const cwd = getCwd(ctx);
196
+ if (!EventAdapter.isBeadsProject(cwd)) return;
197
+ const sessionId = getSessionId(ctx);
198
+
199
+ const markerCheck = await SubprocessRunner.run("bd", ["kv", "get", `memory-gate-done:${sessionId}`], { cwd });
200
+ if (markerCheck.code === 0) {
201
+ await SubprocessRunner.run("bd", ["kv", "clear", `memory-gate-done:${sessionId}`], { cwd });
202
+ await clearSessionMarkers(sessionId, cwd);
203
+ memoryGateFired = false;
204
+ return;
205
+ }
206
+
207
+ if (memoryGateFired) return;
208
+
209
+ const closedIssueId = await getClosedThisSession(sessionId, cwd);
210
+ if (!closedIssueId) return;
211
+
212
+ const closeTimeAcked = await hasIssueMemoryAck(closedIssueId, cwd);
213
+ if (closeTimeAcked) {
214
+ await SubprocessRunner.run("bd", ["kv", "clear", `closed-this-session:${sessionId}`], { cwd });
215
+ memoryGateFired = false;
216
+ return;
217
+ }
218
+
219
+ memoryGateFired = true;
220
+ // No notify — memory gate was injected into bd close tool_result content (silent, agent-visible only).
221
+ };
222
+
223
+ pi.on("agent_end", async (_event, ctx) => {
224
+ await triggerMemoryGateIfNeeded(ctx);
225
+ return undefined;
226
+ });
227
+
228
+ pi.on("session_shutdown", async (_event, ctx) => {
229
+ await triggerMemoryGateIfNeeded(ctx);
230
+ return undefined;
231
+ });
232
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@xtrm/pi-beads",
3
+ "version": "1.0.0",
4
+ "description": "xtrm Pi extension: beads",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "keywords": [
10
+ "pi",
11
+ "extension",
12
+ "xtrm"
13
+ ],
14
+ "author": "xtrm",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@xtrm/pi-core": "^1.0.0"
18
+ }
19
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * oh-pi Compact Header — table-style startup info with dynamic column widths
3
+ */
4
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import { VERSION } from "@mariozechner/pi-coding-agent";
6
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
7
+
8
+ export default function (pi: ExtensionAPI) {
9
+ pi.on("session_start", async (_event, ctx) => {
10
+ if (!ctx.hasUI) return;
11
+
12
+ ctx.ui.setHeader((_tui, theme) => ({
13
+ render(width: number): string[] {
14
+ const d = (s: string) => theme.fg("dim", s);
15
+ const a = (s: string) => theme.fg("accent", s);
16
+
17
+ const cmds = pi.getCommands();
18
+ const prompts = cmds.filter(c => c.source === "prompt").map(c => `/${c.name}`).join(" ");
19
+ const skills = cmds.filter(c => c.source === "skill").map(c => c.name).join(" ");
20
+ const model = ctx.model ? `${ctx.model.id}` : "no model";
21
+ const thinking = pi.getThinkingLevel();
22
+ const provider = ctx.model?.provider ?? "";
23
+
24
+ const pad = (s: string, w: number) => s + " ".repeat(Math.max(0, w - visibleWidth(s)));
25
+ const t = (s: string) => truncateToWidth(s, width);
26
+ const sep = d(" │ ");
27
+
28
+ // Right two columns are fixed width
29
+ const rCol = [
30
+ [d("esc"), a("interrupt"), d("S-tab"), a("thinking")],
31
+ [d("^C"), a("clear/exit"), d("^O"), a("expand")],
32
+ [d("^P"), a("model"), d("^G"), a("editor")],
33
+ [d("/"), a("commands"), d("^V"), a("paste")],
34
+ [d("!"), a("bash"), d(""), a("")],
35
+ ];
36
+ const k1w = 6, v1w = 13, k2w = 6, v2w = 9;
37
+ const rightW = k1w + v1w + 3 + k2w + v2w + 3; // 3 for each sep
38
+
39
+ // Left column gets remaining space
40
+ const leftW = Math.max(20, width - rightW);
41
+ const lk = 9; // label width
42
+
43
+ const lCol = [
44
+ [d("version"), a(`v${VERSION} ${provider}`)],
45
+ [d("model"), a(model)],
46
+ [d("think"), a(thinking)],
47
+ [d(""), d("")],
48
+ [d(""), d("")],
49
+ ];
50
+
51
+ const lines: string[] = [""];
52
+ for (let i = 0; i < 5; i++) {
53
+ const [lk0, lv0] = lCol[i];
54
+ const [rk0, rv0, rk1, rv1] = rCol[i];
55
+ const left = truncateToWidth(pad(lk0, lk) + lv0, leftW);
56
+ const right = pad(rk0, k1w) + pad(rv0, v1w) + sep + pad(rk1, k2w) + rv1;
57
+ lines.push(t(pad(left, leftW) + sep + right));
58
+ }
59
+
60
+ if (prompts) lines.push(t(`${pad(d("prompts"), lk)}${a(prompts)}`));
61
+ if (skills) lines.push(t(`${pad(d("skills"), lk)}${a(skills)}`));
62
+ lines.push(d("─".repeat(width)));
63
+
64
+ return lines;
65
+ },
66
+ invalidate() {},
67
+ }));
68
+ });
69
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@xtrm/pi-compact-header",
3
+ "version": "1.0.0",
4
+ "description": "xtrm Pi extension: compact-header",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "keywords": [
10
+ "pi",
11
+ "extension",
12
+ "xtrm"
13
+ ],
14
+ "author": "xtrm",
15
+ "license": "MIT"
16
+ }
@@ -0,0 +1,52 @@
1
+ import * as fs from "node:fs";
2
+ import * as nodePath from "node:path";
3
+
4
+ import type { ExtensionAPI, ToolCallEvent } from "@mariozechner/pi-coding-agent";
5
+ import { PI_MUTATING_FILE_TOOLS } from "./guard-rules";
6
+
7
+ export class EventAdapter {
8
+ /**
9
+ * Checks if the tool event is a mutating file operation (write, edit, etc).
10
+ */
11
+ static isMutatingFileTool(event: ToolCallEvent<any, any>): boolean {
12
+ return PI_MUTATING_FILE_TOOLS.includes(event.toolName);
13
+ }
14
+
15
+ /**
16
+ * Extracts the target path from a tool input, resolving against the current working directory.
17
+ */
18
+ static extractPathFromToolInput(event: ToolCallEvent<any, any>, cwd: string): string | null {
19
+ const input = event.input;
20
+ if (!input) return null;
21
+
22
+ const pathRaw = input.path || input.file || input.filePath;
23
+ if (typeof pathRaw === "string") {
24
+ return pathRaw; // Usually Pi passes absolute paths anyway or paths relative to root
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Safely formats a block reason string to ensure UI readiness.
32
+ */
33
+ static formatBlockReason(prefix: string, details: string): string {
34
+ return `${prefix}: ${details}`;
35
+ }
36
+ /**
37
+ * Returns true if the given directory is a beads project (has a .beads directory).
38
+ */
39
+ static isBeadsProject(cwd: string): boolean {
40
+ return fs.existsSync(nodePath.join(cwd, ".beads"));
41
+ }
42
+
43
+ /**
44
+ * Parses the summary line from `bd list` output.
45
+ * Returns { open, inProgress } or null if the line is absent.
46
+ */
47
+ static parseBdCounts(output: string): { open: number; inProgress: number } | null {
48
+ const m = output.match(/Total:\s*\d+\s+issues?\s*\((\d+)\s+open,\s*(\d+)\s+in progress\)/);
49
+ if (!m) return null;
50
+ return { open: parseInt(m[1], 10), inProgress: parseInt(m[2], 10) };
51
+ }
52
+ }
@@ -0,0 +1,100 @@
1
+ // Canonical guard-rule constants for Pi extensions.
2
+ // Mirrors hooks/guard-rules.mjs with Pi-specific tool naming where needed.
3
+
4
+ export const PI_MUTATING_FILE_TOOLS = [
5
+ "write",
6
+ "edit",
7
+ "replace_content",
8
+ "replace_lines",
9
+ "delete_lines",
10
+ "insert_at_line",
11
+ "create_text_file",
12
+ "rename_symbol",
13
+ "replace_symbol_body",
14
+ "insert_after_symbol",
15
+ "insert_before_symbol",
16
+ ];
17
+
18
+ export const SAFE_BASH_PREFIXES = [
19
+ // Git read-only
20
+ "git status",
21
+ "git log",
22
+ "git diff",
23
+ "git show",
24
+ "git blame",
25
+ "git branch",
26
+ "git fetch",
27
+ "git remote",
28
+ "git config",
29
+ "git pull",
30
+ "git stash",
31
+ "git worktree",
32
+ "git checkout -b",
33
+ "git switch -c",
34
+ // Tools
35
+ "gh",
36
+ "bd",
37
+ "npx gitnexus",
38
+ "xtrm finish",
39
+ // Read-only filesystem
40
+ "cat",
41
+ "ls",
42
+ "head",
43
+ "tail",
44
+ "pwd",
45
+ "which",
46
+ "type",
47
+ "env",
48
+ "printenv",
49
+ "find",
50
+ "grep",
51
+ "rg",
52
+ "fd",
53
+ "wc",
54
+ "sort",
55
+ "uniq",
56
+ "cut",
57
+ "awk",
58
+ "jq",
59
+ "yq",
60
+ "bat",
61
+ "less",
62
+ "more",
63
+ "file",
64
+ "stat",
65
+ "du",
66
+ "tree",
67
+ ];
68
+
69
+ export const DANGEROUS_BASH_PATTERNS = [
70
+ "sed\\s+-i",
71
+ "echo\\s+[^\\n]*>",
72
+ "printf\\s+[^\\n]*>",
73
+ "cat\\s+[^\\n]*>",
74
+ "tee\\b",
75
+ "(?:^|\\s)(?:vim|nano|vi)\\b",
76
+ "(?:^|\\s)mv\\b",
77
+ "(?:^|\\s)cp\\b",
78
+ "(?:^|\\s)rm\\b",
79
+ "(?:^|\\s)mkdir\\b",
80
+ "(?:^|\\s)touch\\b",
81
+ "(?:^|\\s)chmod\\b",
82
+ "(?:^|\\s)chown\\b",
83
+ ">>",
84
+ "(?:^|\\s)git\\s+add\\b",
85
+ "(?:^|\\s)git\\s+commit\\b",
86
+ "(?:^|\\s)git\\s+merge\\b",
87
+ "(?:^|\\s)git\\s+push\\b",
88
+ "(?:^|\\s)git\\s+reset\\b",
89
+ "(?:^|\\s)git\\s+checkout\\b",
90
+ "(?:^|\\s)git\\s+rebase\\b",
91
+ "(?:^|\\s)git\\s+stash\\b",
92
+ "(?:^|\\s)npm\\s+install\\b",
93
+ "(?:^|\\s)bun\\s+install\\b",
94
+ "(?:^|\\s)bun\\s+add\\b",
95
+ "(?:^|\\s)node\\s+(?:-e|--eval)\\b",
96
+ "(?:^|\\s)bun\\s+(?:-e|--eval)\\b",
97
+ "(?:^|\\s)python\\s+-c\\b",
98
+ "(?:^|\\s)perl\\s+-e\\b",
99
+ "(?:^|\\s)ruby\\s+-e\\b",
100
+ ];
@@ -0,0 +1,3 @@
1
+ export * from "./logger";
2
+ export * from "./runner";
3
+ export * from "./adapter";
@@ -0,0 +1,45 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+
3
+ export interface LoggerOptions {
4
+ namespace: string;
5
+ level?: LogLevel;
6
+ }
7
+
8
+ export class Logger {
9
+ private namespace: string;
10
+ private level: LogLevel;
11
+
12
+ constructor(options: LoggerOptions) {
13
+ this.namespace = options.namespace;
14
+ this.level = options.level || "info";
15
+ }
16
+
17
+ private shouldLog(level: LogLevel): boolean {
18
+ const levels: LogLevel[] = ["debug", "info", "warn", "error"];
19
+ return levels.indexOf(level) >= levels.indexOf(this.level);
20
+ }
21
+
22
+ debug(message: string, ...args: any[]) {
23
+ if (this.shouldLog("debug")) {
24
+ console.debug(`[${this.namespace}] DEBUG: ${message}`, ...args);
25
+ }
26
+ }
27
+
28
+ info(message: string, ...args: any[]) {
29
+ if (this.shouldLog("info")) {
30
+ console.info(`[${this.namespace}] INFO: ${message}`, ...args);
31
+ }
32
+ }
33
+
34
+ warn(message: string, ...args: any[]) {
35
+ if (this.shouldLog("warn")) {
36
+ console.warn(`[${this.namespace}] WARN: ${message}`, ...args);
37
+ }
38
+ }
39
+
40
+ error(message: string, ...args: any[]) {
41
+ if (this.shouldLog("error")) {
42
+ console.error(`[${this.namespace}] ERROR: ${message}`, ...args);
43
+ }
44
+ }
45
+ }