xtrm-tools 0.5.48 → 0.6.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/.claude-plugin/plugin.json +1 -1
- package/cli/dist/index.cjs +6218 -7897
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +5 -1
- package/config/pi/extensions/beads/index.ts +1 -1
- package/config/pi/extensions/beads/package.json +4 -1
- package/config/pi/extensions/core/package.json +18 -0
- package/config/pi/extensions/custom-footer/index.ts +138 -71
- package/config/pi/extensions/custom-footer/package.json +4 -1
- package/config/pi/extensions/quality-gates/index.ts +1 -1
- package/config/pi/extensions/quality-gates/package.json +4 -1
- package/config/pi/extensions/service-skills/index.ts +1 -1
- package/config/pi/extensions/service-skills/package.json +4 -1
- package/config/pi/extensions/session-flow/index.ts +1 -1
- package/config/pi/extensions/session-flow/package.json +4 -1
- package/config/pi/extensions/xtrm-loader/index.ts +1 -1
- package/config/pi/extensions/xtrm-loader/package.json +4 -1
- package/hooks/beads-compact-restore.mjs +11 -3
- package/hooks/beads-compact-save.mjs +13 -1
- package/hooks/tsconfig-cache.json +12 -2
- package/package.json +3 -2
- package/plugins/xtrm-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/xtrm-tools/hooks/beads-compact-restore.mjs +11 -3
- package/plugins/xtrm-tools/hooks/beads-compact-save.mjs +13 -1
- package/plugins/xtrm-tools/hooks/tsconfig-cache.json +12 -2
- package/plugins/xtrm-tools/skills/xt-end/SKILL.md +1 -0
- package/plugins/xtrm-tools/skills/xt-merge/SKILL.md +141 -18
- package/skills/xt-end/SKILL.md +1 -0
- package/skills/xt-merge/SKILL.md +141 -18
package/cli/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtrm-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Claude Code tools installer (skills, hooks, MCP servers)",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
"xtrm": "dist/index.cjs",
|
|
9
9
|
"xt": "dist/index.cjs"
|
|
10
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"config"
|
|
14
|
+
],
|
|
11
15
|
"scripts": {
|
|
12
16
|
"prebuild": "node -e \"if(process.cwd().includes('/.xtrm/worktrees/')){console.error('ERROR: Do not run npm run build from a worktree — dist paths will be contaminated.\\nRun from the main repo: cd <repo-root>/cli && npm run build');process.exit(1)}\"",
|
|
13
17
|
"build": "tsup",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { isToolCallEventType, isBashToolResult } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { SubprocessRunner, EventAdapter } from "
|
|
3
|
+
import { SubprocessRunner, EventAdapter } from "@xtrm/pi-core";
|
|
4
4
|
|
|
5
5
|
export default function (pi: ExtensionAPI) {
|
|
6
6
|
const getCwd = (ctx: any) => ctx.cwd || process.cwd();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrm/pi-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared utilities for xtrm Pi extensions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./lib.ts",
|
|
8
|
+
"./lib": "./lib.ts",
|
|
9
|
+
"./logger": "./logger.ts",
|
|
10
|
+
"./runner": "./runner.ts",
|
|
11
|
+
"./adapter": "./adapter.ts",
|
|
12
|
+
"./guard-rules": "./guard-rules.ts",
|
|
13
|
+
"./session-state": "./session-state.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["pi", "extension", "xtrm"],
|
|
16
|
+
"author": "xtrm",
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* XTRM Custom Footer Extension
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* Line 1: ~/path (branch *+↑) — with git status flags, no session name
|
|
6
|
+
* Line 2: XX%/window | (provider) model • thinking — simplified stats
|
|
7
|
+
* Line 3: ◐ 4843.5 Rework project bootstrap... — beads claim or ○ 6 open
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
-
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
9
|
-
import { basename, relative } from "node:path";
|
|
10
|
-
import { hostname } from "node:os";
|
|
11
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
12
|
|
|
12
|
-
import { SubprocessRunner, EventAdapter } from "
|
|
13
|
+
import { SubprocessRunner, EventAdapter } from "@xtrm/pi-core";
|
|
13
14
|
|
|
14
15
|
export default function (pi: ExtensionAPI) {
|
|
15
16
|
interface BeadState {
|
|
@@ -22,11 +23,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
interface RuntimeState {
|
|
25
|
-
host: string;
|
|
26
|
-
displayDir: string;
|
|
27
26
|
branch: string | null;
|
|
28
27
|
gitStatus: string;
|
|
29
|
-
venv: string | null;
|
|
30
28
|
lastFetch: number;
|
|
31
29
|
}
|
|
32
30
|
|
|
@@ -52,6 +50,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
52
50
|
|
|
53
51
|
const chip = (text: string, bg = CHIP_BG_NEUTRAL): string => `${bg}${CHIP_FG} ${text} ${CHIP_RESET}`;
|
|
54
52
|
|
|
53
|
+
let capturedPi: ExtensionAPI = pi;
|
|
55
54
|
let capturedCtx: any = null;
|
|
56
55
|
let sessionId = "";
|
|
57
56
|
let requestRender: (() => void) | null = null;
|
|
@@ -70,17 +69,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
70
69
|
};
|
|
71
70
|
|
|
72
71
|
let runtimeState: RuntimeState = {
|
|
73
|
-
host: hostname().split(".")[0] || "host",
|
|
74
|
-
displayDir: process.cwd(),
|
|
75
72
|
branch: null,
|
|
76
73
|
gitStatus: "",
|
|
77
|
-
venv: process.env.VIRTUAL_ENV ? basename(process.env.VIRTUAL_ENV) : null,
|
|
78
74
|
lastFetch: 0,
|
|
79
75
|
};
|
|
80
76
|
|
|
81
77
|
const getCwd = () => capturedCtx?.cwd || process.cwd();
|
|
82
78
|
const getShortId = (id: string) => id.split("-").pop() ?? id;
|
|
83
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Parse git status --porcelain output into status flags
|
|
82
|
+
*/
|
|
84
83
|
const parseGitFlags = (porcelain: string): string => {
|
|
85
84
|
let modified = false;
|
|
86
85
|
let staged = false;
|
|
@@ -93,33 +92,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
93
92
|
return `${modified ? "*" : ""}${staged ? "+" : ""}${deleted ? "-" : ""}`;
|
|
94
93
|
};
|
|
95
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Fetch git branch and status
|
|
97
|
+
*/
|
|
96
98
|
const refreshRuntimeState = async () => {
|
|
97
99
|
if (refreshingRuntime || Date.now() - runtimeState.lastFetch < CACHE_TTL) return;
|
|
98
100
|
refreshingRuntime = true;
|
|
99
101
|
const cwd = getCwd();
|
|
100
102
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
let branch: string | null = null;
|
|
104
|
+
let gitStatus = "";
|
|
105
|
+
|
|
103
106
|
const rootResult = await SubprocessRunner.run("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
104
107
|
const repoRoot = rootResult.code === 0 ? rootResult.stdout.trim() : null;
|
|
105
108
|
|
|
106
|
-
const displayDir = repoRoot
|
|
107
|
-
? (() => {
|
|
108
|
-
const relPath = relative(repoRoot, cwd) || ".";
|
|
109
|
-
return relPath === "." ? basename(repoRoot) : `${basename(repoRoot)}/${relPath}`;
|
|
110
|
-
})()
|
|
111
|
-
: (() => {
|
|
112
|
-
const parts = cwd.split("/");
|
|
113
|
-
return parts.length > 2 ? parts.slice(-2).join("/") : cwd;
|
|
114
|
-
})();
|
|
115
|
-
|
|
116
|
-
let branch: string | null = null;
|
|
117
|
-
let gitStatus = "";
|
|
118
109
|
if (repoRoot) {
|
|
119
110
|
const branchResult = await SubprocessRunner.run("git", ["branch", "--show-current"], { cwd });
|
|
120
111
|
branch = branchResult.code === 0 ? branchResult.stdout.trim() || null : null;
|
|
121
112
|
|
|
122
|
-
const porcelainResult = await SubprocessRunner.run(
|
|
113
|
+
const porcelainResult = await SubprocessRunner.run(
|
|
114
|
+
"git",
|
|
115
|
+
["--no-optional-locks", "status", "--porcelain"],
|
|
116
|
+
{ cwd },
|
|
117
|
+
);
|
|
123
118
|
const baseFlags = porcelainResult.code === 0 ? parseGitFlags(porcelainResult.stdout) : "";
|
|
124
119
|
|
|
125
120
|
let upstreamFlags = "";
|
|
@@ -141,11 +136,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
141
136
|
}
|
|
142
137
|
|
|
143
138
|
runtimeState = {
|
|
144
|
-
host,
|
|
145
|
-
displayDir,
|
|
146
139
|
branch,
|
|
147
140
|
gitStatus,
|
|
148
|
-
venv,
|
|
149
141
|
lastFetch: Date.now(),
|
|
150
142
|
};
|
|
151
143
|
requestRender?.();
|
|
@@ -156,6 +148,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
148
|
}
|
|
157
149
|
};
|
|
158
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Format token counts (from original footer)
|
|
153
|
+
*/
|
|
154
|
+
const formatTokens = (count: number): string => {
|
|
155
|
+
if (count < 1000) return count.toString();
|
|
156
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
157
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
158
|
+
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
|
|
159
|
+
return `${Math.round(count / 1000000)}M`;
|
|
160
|
+
};
|
|
161
|
+
|
|
159
162
|
const refreshBeadState = async () => {
|
|
160
163
|
if (refreshingBeads || Date.now() - beadState.lastFetch < CACHE_TTL) return;
|
|
161
164
|
const cwd = getCwd();
|
|
@@ -216,29 +219,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
216
219
|
}
|
|
217
220
|
};
|
|
218
221
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const bg = STATUS_BG[status] ?? CHIP_BG_NEUTRAL;
|
|
224
|
-
return chip(`bd:${shortId}${icon}`, bg);
|
|
225
|
-
}
|
|
226
|
-
if (openCount > 0) return chip(`bd:${openCount}${STATUS_ICONS.open}`);
|
|
227
|
-
return "";
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const buildIssueLine = (width: number, theme: any): string => {
|
|
222
|
+
/**
|
|
223
|
+
* Build beads line: ◐ 4843.5 Rework project bootstrap... or ○ 6 open
|
|
224
|
+
*/
|
|
225
|
+
const buildBeadsLine = (width: number, theme: any): string => {
|
|
231
226
|
const { shortId, claimTitle, status, openCount } = beadState;
|
|
227
|
+
|
|
228
|
+
// Claimed: ◐ 4843.5 Rework project bootstrap, verificat...
|
|
232
229
|
if (shortId && claimTitle && status) {
|
|
233
230
|
const icon = STATUS_ICONS[status] ?? "◐";
|
|
234
231
|
const prefix = `${icon} ${shortId} `;
|
|
235
232
|
const title = theme.fg("muted", claimTitle);
|
|
236
233
|
return truncateToWidth(`${prefix}${title}`, width);
|
|
237
234
|
}
|
|
235
|
+
|
|
236
|
+
// Unclaimed with open issues: ○ 6 open
|
|
238
237
|
if (openCount > 0) {
|
|
239
238
|
return truncateToWidth(`○ ${openCount} open`, width);
|
|
240
239
|
}
|
|
241
|
-
|
|
240
|
+
|
|
241
|
+
// No open issues: ○ no open issues
|
|
242
|
+
return truncateToWidth(`○ no open issues`, width);
|
|
242
243
|
};
|
|
243
244
|
|
|
244
245
|
let footerReapplyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -262,36 +263,103 @@ export default function (pi: ExtensionAPI) {
|
|
|
262
263
|
refreshRuntimeState().catch(() => {});
|
|
263
264
|
refreshBeadState().catch(() => {});
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
// === LINE 1: ~/path (branch *+↑) ===
|
|
267
|
+
// Like original, no session name, but with git status flags
|
|
268
|
+
let pwd = process.cwd();
|
|
269
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
270
|
+
if (home && pwd.startsWith(home)) {
|
|
271
|
+
pwd = `~${pwd.slice(home.length)}`;
|
|
272
|
+
}
|
|
268
273
|
|
|
274
|
+
// Use runtimeState branch (with git status) or fallback to footerData
|
|
275
|
+
const branch = runtimeState.branch || footerData.getGitBranch();
|
|
276
|
+
if (branch) {
|
|
277
|
+
const branchWithStatus = runtimeState.gitStatus
|
|
278
|
+
? `${branch} ${runtimeState.gitStatus}`
|
|
279
|
+
: branch;
|
|
280
|
+
pwd = `${pwd} (${branchWithStatus})`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const pwdLine = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
|
|
284
|
+
|
|
285
|
+
// === LINE 2: XX%/window (provider) model • thinking ===
|
|
269
286
|
const usage = ctx.getContextUsage();
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
287
|
+
const model = ctx.model;
|
|
288
|
+
|
|
289
|
+
const contextWindow = usage?.contextWindow ?? model?.contextWindow ?? 0;
|
|
290
|
+
const contextPercentValue = usage?.percent ?? 0;
|
|
291
|
+
const contextPercent = usage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
292
|
+
|
|
293
|
+
// Build left side: context %/window (color-coded like original)
|
|
294
|
+
const contextDisplay =
|
|
295
|
+
contextPercent === "?"
|
|
296
|
+
? `?/${formatTokens(contextWindow)}`
|
|
297
|
+
: `${contextPercent}%/${formatTokens(contextWindow)}`;
|
|
298
|
+
|
|
299
|
+
let statsLeft: string;
|
|
300
|
+
if (contextPercentValue > 90) {
|
|
301
|
+
statsLeft = theme.fg("error", contextDisplay);
|
|
302
|
+
} else if (contextPercentValue > 70) {
|
|
303
|
+
statsLeft = theme.fg("warning", contextDisplay);
|
|
304
|
+
} else {
|
|
305
|
+
statsLeft = contextDisplay;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Build right side: (provider) model • thinking
|
|
309
|
+
const modelName = model?.id || "no-model";
|
|
310
|
+
const providerCount = footerData.getAvailableProviderCount();
|
|
311
|
+
|
|
312
|
+
// Thinking level if model supports reasoning
|
|
313
|
+
let rightSideWithoutProvider = modelName;
|
|
314
|
+
if (model?.reasoning) {
|
|
315
|
+
const thinkingLevel = capturedPi.getThinkingLevel() || "off";
|
|
316
|
+
rightSideWithoutProvider =
|
|
317
|
+
thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Prepend provider if >1
|
|
321
|
+
let rightSide = rightSideWithoutProvider;
|
|
322
|
+
if (providerCount > 1 && model) {
|
|
323
|
+
rightSide = `(${model.provider}) ${rightSideWithoutProvider}`;
|
|
324
|
+
if (visibleWidth(statsLeft) + 3 + visibleWidth(rightSide) > width) {
|
|
325
|
+
rightSide = rightSideWithoutProvider;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Keep provider/model adjacent to usage (no right-bound alignment)
|
|
330
|
+
const separator = " ";
|
|
331
|
+
|
|
332
|
+
// Calculate layout
|
|
333
|
+
let leftWidth = visibleWidth(statsLeft);
|
|
334
|
+
const separatorWidth = visibleWidth(separator);
|
|
335
|
+
|
|
336
|
+
// Check if left side too wide
|
|
337
|
+
if (leftWidth > width - separatorWidth) {
|
|
338
|
+
statsLeft = truncateToWidth(statsLeft, width - separatorWidth, "...");
|
|
339
|
+
leftWidth = visibleWidth(statsLeft);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Truncate right side to remaining width and place immediately after separator
|
|
343
|
+
const availableForRight = width - leftWidth - separatorWidth;
|
|
344
|
+
let line2: string;
|
|
345
|
+
if (availableForRight > 0) {
|
|
346
|
+
const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
|
|
347
|
+
line2 = statsLeft + separator + truncatedRight;
|
|
348
|
+
} else {
|
|
349
|
+
line2 = truncateToWidth(statsLeft, width, "");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Apply dim to each part separately (like original footer)
|
|
353
|
+
// statsLeft may contain color codes for context % that end with reset
|
|
354
|
+
const dimLeft = theme.fg("dim", statsLeft);
|
|
355
|
+
const dimSep = theme.fg("dim", separator);
|
|
356
|
+
const remainder = line2.slice(leftWidth + separatorWidth);
|
|
357
|
+
const dimRemainder = theme.fg("dim", remainder);
|
|
358
|
+
|
|
359
|
+
// === LINE 3: ◐ 4843.5 Rework project bootstrap... ===
|
|
360
|
+
const line3 = buildBeadsLine(width, theme);
|
|
361
|
+
|
|
362
|
+
return [pwdLine, dimLeft + dimSep + dimRemainder, line3];
|
|
295
363
|
},
|
|
296
364
|
};
|
|
297
365
|
});
|
|
@@ -325,7 +393,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
325
393
|
});
|
|
326
394
|
|
|
327
395
|
pi.on("model_select", async (_event, ctx) => {
|
|
328
|
-
runtimeState.lastFetch = 0;
|
|
329
396
|
scheduleFooterReapply(ctx);
|
|
330
397
|
});
|
|
331
398
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { isBashToolResult } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import { SubprocessRunner, EventAdapter } from "
|
|
3
|
+
import { SubprocessRunner, EventAdapter } from "@xtrm/pi-core";
|
|
4
4
|
|
|
5
5
|
function isClaimCommand(command: string): { isClaim: boolean; issueId: string | null } {
|
|
6
6
|
if (!/\bbd\s+update\b/.test(command) || !/--claim\b/.test(command)) {
|
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { Logger } from "
|
|
5
|
+
import { Logger } from "@xtrm/pi-core";
|
|
6
6
|
|
|
7
7
|
const logger = new Logger({ namespace: "xtrm-loader" });
|
|
8
8
|
|
|
@@ -21,12 +21,14 @@ const lastActivePath = path.join(cwd, '.beads', '.last_active');
|
|
|
21
21
|
if (!existsSync(lastActivePath)) process.exit(0);
|
|
22
22
|
|
|
23
23
|
let ids = [];
|
|
24
|
+
let serenaProject = null;
|
|
24
25
|
|
|
25
26
|
try {
|
|
26
27
|
const raw = readFileSync(lastActivePath, 'utf8').trim();
|
|
27
28
|
if (raw.startsWith('{')) {
|
|
28
29
|
const parsed = JSON.parse(raw);
|
|
29
30
|
ids = Array.isArray(parsed.ids) ? parsed.ids.filter(Boolean) : [];
|
|
31
|
+
serenaProject = parsed.serenaProject ?? null;
|
|
30
32
|
} else {
|
|
31
33
|
// Backward compatibility: legacy newline format
|
|
32
34
|
ids = raw.split('\n').filter(Boolean);
|
|
@@ -53,14 +55,20 @@ for (const id of ids) {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
if (restored > 0) {
|
|
57
|
-
const
|
|
58
|
+
if (restored > 0 || serenaProject) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (restored > 0) {
|
|
61
|
+
parts.push(`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction. Check \`bd list\` for details.`);
|
|
62
|
+
}
|
|
63
|
+
if (serenaProject) {
|
|
64
|
+
parts.push(`Serena was active on project "${serenaProject}" — re-activate with activate_project("${serenaProject}") before any symbol lookups.`);
|
|
65
|
+
}
|
|
58
66
|
|
|
59
67
|
process.stdout.write(
|
|
60
68
|
JSON.stringify({
|
|
61
69
|
hookSpecificOutput: {
|
|
62
70
|
hookEventName: 'SessionStart',
|
|
63
|
-
additionalSystemPrompt:
|
|
71
|
+
additionalSystemPrompt: parts.join(' '),
|
|
64
72
|
},
|
|
65
73
|
}) + '\n',
|
|
66
74
|
);
|
|
@@ -44,7 +44,19 @@ const bundle = {
|
|
|
44
44
|
savedAt: new Date().toISOString(),
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Detect active Serena project
|
|
48
|
+
const serenaProjectYml = path.join(cwd, '.serena', 'project.yml');
|
|
49
|
+
if (existsSync(serenaProjectYml)) {
|
|
50
|
+
try {
|
|
51
|
+
const yml = readFileSync(serenaProjectYml, 'utf8');
|
|
52
|
+
const m = yml.match(/^project_name:\s*["']?([^"'\n]+)["']?/m);
|
|
53
|
+
if (m) bundle.serenaProject = m[1].trim();
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore — not critical
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (bundle.ids.length === 0 && !bundle.serenaProject) process.exit(0);
|
|
48
60
|
|
|
49
61
|
writeFileSync(path.join(beadsDir, '.last_active'), JSON.stringify(bundle, null, 2) + '\n', 'utf8');
|
|
50
62
|
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hashes": {
|
|
3
|
-
|
|
2
|
+
"hashes": {
|
|
3
|
+
"/home/dawid/projects/specialists/tsconfig.json": "84f37aa64b949b7aaba41974cb6137341a36730b7d3e174803cb9289eae1e7c7"
|
|
4
|
+
},
|
|
5
|
+
"mappings": {
|
|
6
|
+
"src/**/*": {
|
|
7
|
+
"configPath": "/home/dawid/projects/specialists/tsconfig.json",
|
|
8
|
+
"excludes": [
|
|
9
|
+
"node_modules",
|
|
10
|
+
"dist"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtrm-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Claude Code tools installer (skills, hooks, MCP servers)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"workspaces": [
|
|
8
|
-
"cli"
|
|
8
|
+
"cli",
|
|
9
|
+
"config/pi/extensions/core"
|
|
9
10
|
],
|
|
10
11
|
"bin": {
|
|
11
12
|
"xtrm": "cli/dist/index.cjs",
|
|
@@ -21,12 +21,14 @@ const lastActivePath = path.join(cwd, '.beads', '.last_active');
|
|
|
21
21
|
if (!existsSync(lastActivePath)) process.exit(0);
|
|
22
22
|
|
|
23
23
|
let ids = [];
|
|
24
|
+
let serenaProject = null;
|
|
24
25
|
|
|
25
26
|
try {
|
|
26
27
|
const raw = readFileSync(lastActivePath, 'utf8').trim();
|
|
27
28
|
if (raw.startsWith('{')) {
|
|
28
29
|
const parsed = JSON.parse(raw);
|
|
29
30
|
ids = Array.isArray(parsed.ids) ? parsed.ids.filter(Boolean) : [];
|
|
31
|
+
serenaProject = parsed.serenaProject ?? null;
|
|
30
32
|
} else {
|
|
31
33
|
// Backward compatibility: legacy newline format
|
|
32
34
|
ids = raw.split('\n').filter(Boolean);
|
|
@@ -53,14 +55,20 @@ for (const id of ids) {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
if (restored > 0) {
|
|
57
|
-
const
|
|
58
|
+
if (restored > 0 || serenaProject) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (restored > 0) {
|
|
61
|
+
parts.push(`Restored ${restored} in_progress issue${restored === 1 ? '' : 's'} from last session before compaction. Check \`bd list\` for details.`);
|
|
62
|
+
}
|
|
63
|
+
if (serenaProject) {
|
|
64
|
+
parts.push(`Serena was active on project "${serenaProject}" — re-activate with activate_project("${serenaProject}") before any symbol lookups.`);
|
|
65
|
+
}
|
|
58
66
|
|
|
59
67
|
process.stdout.write(
|
|
60
68
|
JSON.stringify({
|
|
61
69
|
hookSpecificOutput: {
|
|
62
70
|
hookEventName: 'SessionStart',
|
|
63
|
-
additionalSystemPrompt:
|
|
71
|
+
additionalSystemPrompt: parts.join(' '),
|
|
64
72
|
},
|
|
65
73
|
}) + '\n',
|
|
66
74
|
);
|
|
@@ -44,7 +44,19 @@ const bundle = {
|
|
|
44
44
|
savedAt: new Date().toISOString(),
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Detect active Serena project
|
|
48
|
+
const serenaProjectYml = path.join(cwd, '.serena', 'project.yml');
|
|
49
|
+
if (existsSync(serenaProjectYml)) {
|
|
50
|
+
try {
|
|
51
|
+
const yml = readFileSync(serenaProjectYml, 'utf8');
|
|
52
|
+
const m = yml.match(/^project_name:\s*["']?([^"'\n]+)["']?/m);
|
|
53
|
+
if (m) bundle.serenaProject = m[1].trim();
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore — not critical
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (bundle.ids.length === 0 && !bundle.serenaProject) process.exit(0);
|
|
48
60
|
|
|
49
61
|
writeFileSync(path.join(beadsDir, '.last_active'), JSON.stringify(bundle, null, 2) + '\n', 'utf8');
|
|
50
62
|
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hashes": {
|
|
3
|
-
|
|
2
|
+
"hashes": {
|
|
3
|
+
"/home/dawid/projects/specialists/tsconfig.json": "84f37aa64b949b7aaba41974cb6137341a36730b7d3e174803cb9289eae1e7c7"
|
|
4
|
+
},
|
|
5
|
+
"mappings": {
|
|
6
|
+
"src/**/*": {
|
|
7
|
+
"configPath": "/home/dawid/projects/specialists/tsconfig.json",
|
|
8
|
+
"excludes": [
|
|
9
|
+
"node_modules",
|
|
10
|
+
"dist"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
}
|
|
@@ -19,6 +19,7 @@ Default to **autonomous execution**:
|
|
|
19
19
|
- do not ask the user routine clarification questions
|
|
20
20
|
- prefer deterministic fallbacks over conversational review
|
|
21
21
|
- only stop when a real blocker prevents safe progress
|
|
22
|
+
- **always invoke `xt end --yes`** — never call `xt end` without this flag; the bare command prompts interactively for worktree removal which blocks autonomous execution
|
|
22
23
|
|
|
23
24
|
## Success States
|
|
24
25
|
|