xtrm-tools 0.5.26 → 0.5.28

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.
@@ -1,324 +0,0 @@
1
- /**
2
- * Pure utility functions for plan mode.
3
- * Extracted for testability.
4
- */
5
-
6
- import { existsSync } from "node:fs";
7
- import { join } from "node:path";
8
- import { spawnSync } from "node:child_process";
9
-
10
- // Destructive commands blocked in plan mode
11
- const DESTRUCTIVE_PATTERNS = [
12
- /\brm\b/i,
13
- /\brmdir\b/i,
14
- /\bmv\b/i,
15
- /\bcp\b/i,
16
- /\bmkdir\b/i,
17
- /\btouch\b/i,
18
- /\bchmod\b/i,
19
- /\bchown\b/i,
20
- /\bchgrp\b/i,
21
- /\bln\b/i,
22
- /\btee\b/i,
23
- /\btruncate\b/i,
24
- /\bdd\b/i,
25
- /\bshred\b/i,
26
- /(^|[^<])>(?!>)/,
27
- />>/,
28
- /\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
29
- /\byarn\s+(add|remove|install|publish)/i,
30
- /\bpnpm\s+(add|remove|install|publish)/i,
31
- /\bpip\s+(install|uninstall)/i,
32
- /\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
33
- /\bbrew\s+(install|uninstall|upgrade)/i,
34
- /\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
35
- /\bsudo\b/i,
36
- /\bsu\b/i,
37
- /\bkill\b/i,
38
- /\bpkill\b/i,
39
- /\bkillall\b/i,
40
- /\breboot\b/i,
41
- /\bshutdown\b/i,
42
- /\bsystemctl\s+(start|stop|restart|enable|disable)/i,
43
- /\bservice\s+\S+\s+(start|stop|restart)/i,
44
- /\b(vim?|nano|emacs|code|subl)\b/i,
45
- ];
46
-
47
- // Safe read-only commands allowed in plan mode
48
- const SAFE_PATTERNS = [
49
- /^\s*cat\b/,
50
- /^\s*head\b/,
51
- /^\s*tail\b/,
52
- /^\s*less\b/,
53
- /^\s*more\b/,
54
- /^\s*grep\b/,
55
- /^\s*find\b/,
56
- /^\s*ls\b/,
57
- /^\s*pwd\b/,
58
- /^\s*echo\b/,
59
- /^\s*printf\b/,
60
- /^\s*wc\b/,
61
- /^\s*sort\b/,
62
- /^\s*uniq\b/,
63
- /^\s*diff\b/,
64
- /^\s*file\b/,
65
- /^\s*stat\b/,
66
- /^\s*du\b/,
67
- /^\s*df\b/,
68
- /^\s*tree\b/,
69
- /^\s*which\b/,
70
- /^\s*whereis\b/,
71
- /^\s*type\b/,
72
- /^\s*env\b/,
73
- /^\s*printenv\b/,
74
- /^\s*uname\b/,
75
- /^\s*whoami\b/,
76
- /^\s*id\b/,
77
- /^\s*date\b/,
78
- /^\s*cal\b/,
79
- /^\s*uptime\b/,
80
- /^\s*ps\b/,
81
- /^\s*top\b/,
82
- /^\s*htop\b/,
83
- /^\s*free\b/,
84
- /^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
85
- /^\s*git\s+ls-/i,
86
- /^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
87
- /^\s*yarn\s+(list|info|why|audit)/i,
88
- /^\s*node\s+--version/i,
89
- /^\s*python\s+--version/i,
90
- /^\s*curl\s/i,
91
- /^\s*wget\s+-O\s*-/i,
92
- /^\s*jq\b/,
93
- /^\s*sed\s+-n/i,
94
- /^\s*awk\b/,
95
- /^\s*rg\b/,
96
- /^\s*fd\b/,
97
- /^\s*bat\b/,
98
- /^\s*exa\b/,
99
- ];
100
-
101
- export function isSafeCommand(command: string): boolean {
102
- const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command));
103
- const isSafe = SAFE_PATTERNS.some((p) => p.test(command));
104
- return !isDestructive && isSafe;
105
- }
106
-
107
- export interface TodoItem {
108
- step: number;
109
- text: string;
110
- completed: boolean;
111
- }
112
-
113
- export function cleanStepText(text: string): string {
114
- let cleaned = text
115
- .replace(/\*{1,2}([^*]+)\*{1,2}/g, "$1") // Remove bold/italic
116
- .replace(/`([^`]+)`/g, "$1") // Remove code
117
- .replace(
118
- /^(Use|Run|Execute|Create|Write|Read|Check|Verify|Update|Modify|Add|Remove|Delete|Install)\s+(the\s+)?/i,
119
- "",
120
- )
121
- .replace(/\s+/g, " ")
122
- .trim();
123
-
124
- if (cleaned.length > 0) {
125
- cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
126
- }
127
- if (cleaned.length > 50) {
128
- cleaned = `${cleaned.slice(0, 47)}...`;
129
- }
130
- return cleaned;
131
- }
132
-
133
- export function extractTodoItems(message: string): TodoItem[] {
134
- const items: TodoItem[] = [];
135
- const headerMatch = message.match(/\*{0,2}Plan:\*{0,2}\s*\n/i);
136
- if (!headerMatch) return items;
137
-
138
- const planSection = message.slice(message.indexOf(headerMatch[0]) + headerMatch[0].length);
139
- const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm;
140
-
141
- const matches = Array.from(planSection.matchAll(numberedPattern));
142
- for (const match of matches) {
143
- const text = match[2]
144
- .trim()
145
- .replace(/\*{1,2}$/, "")
146
- .trim();
147
- if (text.length > 5 && !text.startsWith("`") && !text.startsWith("/") && !text.startsWith("-")) {
148
- const cleaned = cleanStepText(text);
149
- if (cleaned.length > 3) {
150
- items.push({ step: items.length + 1, text: cleaned, completed: false });
151
- }
152
- }
153
- }
154
- return items;
155
- }
156
-
157
- export function extractDoneSteps(message: string): number[] {
158
- const steps: number[] = [];
159
- const matches = Array.from(message.matchAll(/\[DONE:(\d+)\]/gi));
160
- for (const match of matches) {
161
- const step = Number(match[1]);
162
- if (Number.isFinite(step)) steps.push(step);
163
- }
164
- return steps;
165
- }
166
-
167
- export function markCompletedSteps(text: string, items: TodoItem[]): number {
168
- const doneSteps = extractDoneSteps(text);
169
- for (const step of doneSteps) {
170
- const item = items.find((t) => t.step === step);
171
- if (item) item.completed = true;
172
- }
173
- return doneSteps.length;
174
- }
175
-
176
- // =============================================================================
177
- // bd (beads) Integration Functions
178
- // =============================================================================
179
-
180
- /**
181
- * Extract short ID from full bd issue ID.
182
- * Example: "jaggers-agent-tools-xr9b.1" → "xr9b.1"
183
- */
184
- export function getShortId(fullId: string): string {
185
- const parts = fullId.split("-");
186
- // Last part is the ID (e.g., "xr9b.1")
187
- return parts[parts.length - 1];
188
- }
189
-
190
- /**
191
- * Check if a directory is a beads project (has .beads directory).
192
- */
193
- export function isBeadsProject(cwd: string): boolean {
194
- return existsSync(join(cwd, ".beads"));
195
- }
196
-
197
- /**
198
- * Derive epic title from user prompt or conversation messages.
199
- */
200
- export function deriveEpicTitle(messages: Array<{ role: string; content?: unknown }>): string {
201
- // Find the last user message
202
- for (let i = messages.length - 1; i >= 0; i--) {
203
- const msg = messages[i];
204
- if (msg.role === "user") {
205
- const content = msg.content;
206
- if (typeof content === "string") {
207
- // Extract first sentence or first 50 chars
208
- const firstSentence = content.split(/[.!?\n]/)[0].trim();
209
- if (firstSentence.length > 10 && firstSentence.length < 80) {
210
- return firstSentence;
211
- }
212
- if (firstSentence.length >= 80) {
213
- return `${firstSentence.slice(0, 77)}...`;
214
- }
215
- }
216
- }
217
- }
218
- return "Plan execution";
219
- }
220
-
221
- /**
222
- * Run a bd command and return the result.
223
- */
224
- function runBd(args: string[], cwd: string): { stdout: string; stderr: string; status: number } {
225
- const result = spawnSync("bd", args, {
226
- cwd,
227
- encoding: "utf8",
228
- timeout: 30000,
229
- });
230
- return {
231
- stdout: result.stdout || "",
232
- stderr: result.stderr || "",
233
- status: result.status ?? 1,
234
- };
235
- }
236
-
237
- /**
238
- * Create an epic in bd.
239
- */
240
- export function bdCreateEpic(title: string, cwd: string): { id: string; title: string } | null {
241
- const result = runBd(["create", title, "-t", "epic", "-p", "1", "--json"], cwd);
242
- if (result.status === 0) {
243
- try {
244
- const data = JSON.parse(result.stdout);
245
- if (Array.isArray(data) && data[0]) {
246
- return { id: data[0].id, title: data[0].title };
247
- }
248
- } catch {
249
- // Parse the ID from stdout if JSON parse fails
250
- const match = result.stdout.match(/Created issue:\s*(\S+)/);
251
- if (match) {
252
- return { id: match[1], title };
253
- }
254
- }
255
- }
256
- return null;
257
- }
258
-
259
- /**
260
- * Create a task issue in bd under an epic.
261
- */
262
- export function bdCreateIssue(
263
- title: string,
264
- description: string,
265
- parentId: string,
266
- cwd: string,
267
- ): { id: string; title: string } | null {
268
- const result = runBd(
269
- ["create", title, "-t", "task", "-p", "1", "--parent", parentId, "-d", description, "--json"],
270
- cwd,
271
- );
272
- if (result.status === 0) {
273
- try {
274
- const data = JSON.parse(result.stdout);
275
- if (Array.isArray(data) && data[0]) {
276
- return { id: data[0].id, title: data[0].title };
277
- }
278
- } catch {
279
- const match = result.stdout.match(/Created issue:\s*(\S+)/);
280
- if (match) {
281
- return { id: match[1], title };
282
- }
283
- }
284
- }
285
- return null;
286
- }
287
-
288
- /**
289
- * Claim an issue in bd.
290
- */
291
- export function bdClaim(issueId: string, cwd: string): boolean {
292
- const result = runBd(["update", issueId, "--claim"], cwd);
293
- return result.status === 0;
294
- }
295
-
296
- /**
297
- * Result of creating plan issues.
298
- */
299
- export interface PlanIssuesResult {
300
- epic: { id: string; title: string };
301
- issues: Array<{ id: string; title: string }>;
302
- }
303
-
304
- /**
305
- * Create an epic and issues from todo items.
306
- */
307
- export function createPlanIssues(
308
- epicTitle: string,
309
- todos: TodoItem[],
310
- cwd: string,
311
- ): PlanIssuesResult | null {
312
- const epic = bdCreateEpic(epicTitle, cwd);
313
- if (!epic) return null;
314
-
315
- const issues: Array<{ id: string; title: string }> = [];
316
- for (const todo of todos) {
317
- const issue = bdCreateIssue(todo.text, `Step ${todo.step} of plan: ${epicTitle}`, epic.id, cwd);
318
- if (issue) {
319
- issues.push(issue);
320
- }
321
- }
322
-
323
- return { epic, issues };
324
- }