xtrm-tools 2.4.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "2.4.3",
3
+ "version": "2.4.6",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
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",
@@ -78,7 +78,7 @@ export default function (pi: ExtensionAPI) {
78
78
  if (inProgress) {
79
79
  return {
80
80
  block: true,
81
- reason: `Active claim [${claim}] — close it first.\n bd close ${claim}\n git push -u origin <feature-branch>\n gh pr create --fill && gh pr merge --squash\n`,
81
+ 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`,
82
82
  };
83
83
  }
84
84
  }
@@ -16,6 +16,7 @@ export const PI_MUTATING_FILE_TOOLS = [
16
16
  ];
17
17
 
18
18
  export const SAFE_BASH_PREFIXES = [
19
+ // Git read-only
19
20
  "git status",
20
21
  "git log",
21
22
  "git diff",
@@ -30,10 +31,41 @@ export const SAFE_BASH_PREFIXES = [
30
31
  "git worktree",
31
32
  "git checkout -b",
32
33
  "git switch -c",
34
+ // Tools
33
35
  "gh",
34
36
  "bd",
35
- "touch .beads/",
36
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
+ // Allowed writes (specific paths)
68
+ "touch .beads/",
37
69
  ];
38
70
 
39
71
  export const DANGEROUS_BASH_PATTERNS = [
@@ -67,4 +99,4 @@ export const DANGEROUS_BASH_PATTERNS = [
67
99
  "(?:^|\\s)python\\s+-c\\b",
68
100
  "(?:^|\\s)perl\\s+-e\\b",
69
101
  "(?:^|\\s)ruby\\s+-e\\b",
70
- ];
102
+ ];
@@ -132,14 +132,14 @@ export default function (pi: ExtensionAPI) {
132
132
  const modelId = ctx.model?.id || "no-model";
133
133
  const modelChip = chip(modelId);
134
134
 
135
- const sep = theme.fg("dim", " | ");
135
+ const sep = " ";
136
136
 
137
137
  const brandModel = `${brand} ${modelChip}`;
138
- const leftParts = [brandModel, usageStr, cwdStr];
139
-
140
138
  const beadChip = buildBeadChip();
141
- const branchWithChip = branchStr ? `${branchStr} ${beadChip}`.trim() : beadChip;
142
- if (branchWithChip) leftParts.push(branchWithChip);
139
+ const leftParts = [brandModel, usageStr];
140
+ if (beadChip) leftParts.push(beadChip);
141
+ leftParts.push(cwdStr);
142
+ if (branchStr) leftParts.push(branchStr);
143
143
 
144
144
  const left = leftParts.join(sep);
145
145
  return [truncateToWidth(left, width)];
@@ -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
+ }
@@ -16,7 +16,17 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
16
16
  import type { AssistantMessage, TextContent } from "@mariozechner/pi-ai";
17
17
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
18
18
  import { Key } from "@mariozechner/pi-tui";
19
- import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from "./utils.js";
19
+ import {
20
+ extractTodoItems,
21
+ isSafeCommand,
22
+ markCompletedSteps,
23
+ type TodoItem,
24
+ getShortId,
25
+ isBeadsProject,
26
+ deriveEpicTitle,
27
+ createPlanIssues,
28
+ bdClaim,
29
+ } from "./utils.js";
20
30
 
21
31
  // Tools
22
32
  const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"];
@@ -39,6 +49,8 @@ export default function planModeExtension(pi: ExtensionAPI): void {
39
49
  let planModeEnabled = false;
40
50
  let executionMode = false;
41
51
  let todoItems: TodoItem[] = [];
52
+ let epicId: string | null = null;
53
+ let issueIds: Map<number, string> = new Map(); // step -> issue ID
42
54
 
43
55
  pi.registerFlag("plan", {
44
56
  description: "Start in plan mode (read-only exploration)",
@@ -57,15 +69,17 @@ export default function planModeExtension(pi: ExtensionAPI): void {
57
69
  ctx.ui.setStatus("plan-mode", undefined);
58
70
  }
59
71
 
60
- // Widget showing todo list
72
+ // Widget showing todo list with bd issue IDs
61
73
  if (executionMode && todoItems.length > 0) {
62
74
  const lines = todoItems.map((item) => {
75
+ const issueId = issueIds.get(item.step);
76
+ const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
63
77
  if (item.completed) {
64
78
  return (
65
- ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
79
+ ctx.ui.theme.fg("success", "☑ ") + ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(`${idLabel}${item.text}`))
66
80
  );
67
81
  }
68
- return `${ctx.ui.theme.fg("muted", "☐ ")}${item.text}`;
82
+ return `${ctx.ui.theme.fg("muted", "☐ ")}${idLabel}${item.text}`;
69
83
  });
70
84
  ctx.ui.setWidget("plan-todos", lines);
71
85
  } else {
@@ -77,6 +91,8 @@ export default function planModeExtension(pi: ExtensionAPI): void {
77
91
  planModeEnabled = !planModeEnabled;
78
92
  executionMode = false;
79
93
  todoItems = [];
94
+ epicId = null;
95
+ issueIds.clear();
80
96
 
81
97
  if (planModeEnabled) {
82
98
  pi.setActiveTools(PLAN_MODE_TOOLS);
@@ -93,6 +109,8 @@ export default function planModeExtension(pi: ExtensionAPI): void {
93
109
  enabled: planModeEnabled,
94
110
  todos: todoItems,
95
111
  executing: executionMode,
112
+ epicId,
113
+ issueIds: Object.fromEntries(issueIds),
96
114
  });
97
115
  }
98
116
 
@@ -108,7 +126,11 @@ export default function planModeExtension(pi: ExtensionAPI): void {
108
126
  ctx.ui.notify("No todos. Create a plan first with /plan", "info");
109
127
  return;
110
128
  }
111
- const list = todoItems.map((item, i) => `${i + 1}. ${item.completed ? "✓" : "○"} ${item.text}`).join("\n");
129
+ const list = todoItems.map((item, i) => {
130
+ const issueId = issueIds.get(item.step);
131
+ const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
132
+ return `${i + 1}. ${item.completed ? "✓" : "○"} ${idLabel}${item.text}`;
133
+ }).join("\n");
112
134
  ctx.ui.notify(`Plan Progress:\n${list}`, "info");
113
135
  },
114
136
  });
@@ -187,7 +209,11 @@ Do NOT attempt to make changes - just describe what you would do.`,
187
209
 
188
210
  if (executionMode && todoItems.length > 0) {
189
211
  const remaining = todoItems.filter((t) => !t.completed);
190
- const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
212
+ const todoList = remaining.map((t) => {
213
+ const issueId = issueIds.get(t.step);
214
+ const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
215
+ return `${t.step}. ${idLabel}${t.text}`;
216
+ }).join("\n");
191
217
  return {
192
218
  message: {
193
219
  customType: "plan-execution-context",
@@ -221,13 +247,19 @@ After completing a step, include a [DONE:n] tag in your response.`,
221
247
  // Check if execution is complete
222
248
  if (executionMode && todoItems.length > 0) {
223
249
  if (todoItems.every((t) => t.completed)) {
224
- const completedList = todoItems.map((t) => `~~${t.text}~~`).join("\n");
250
+ const completedList = todoItems.map((t) => {
251
+ const issueId = issueIds.get(t.step);
252
+ const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
253
+ return `~~${idLabel}${t.text}~~`;
254
+ }).join("\n");
225
255
  pi.sendMessage(
226
256
  { customType: "plan-complete", content: `**Plan Complete!** ✓\n\n${completedList}`, display: true },
227
257
  { triggerTurn: false },
228
258
  );
229
259
  executionMode = false;
230
260
  todoItems = [];
261
+ epicId = null;
262
+ issueIds.clear();
231
263
  pi.setActiveTools(NORMAL_MODE_TOOLS);
232
264
  updateStatus(ctx);
233
265
  persistState(); // Save cleared state so resume doesn't restore old execution mode
@@ -246,31 +278,72 @@ After completing a step, include a [DONE:n] tag in your response.`,
246
278
  }
247
279
  }
248
280
 
249
- // Show plan steps and prompt for next action
281
+ // Auto-create epic + issues if in a beads project
282
+ const cwd = ctx.cwd || process.cwd();
283
+ if (todoItems.length > 0 && isBeadsProject(cwd) && !epicId) {
284
+ // Derive epic title from user prompt or first step
285
+ const epicTitle = deriveEpicTitle(event.messages);
286
+
287
+ ctx.ui.notify("Creating epic and issues in bd...", "info");
288
+ const result = await createPlanIssues(epicTitle, todoItems, cwd);
289
+
290
+ if (result) {
291
+ epicId = result.epic.id;
292
+ for (const issue of result.issues) {
293
+ // Match issue to todo by title similarity
294
+ const matchingTodo = todoItems.find(t =>
295
+ issue.title.toLowerCase().includes(t.text.toLowerCase().slice(0, 30)) ||
296
+ t.text.toLowerCase().includes(issue.title.toLowerCase().slice(0, 30))
297
+ );
298
+ if (matchingTodo) {
299
+ issueIds.set(matchingTodo.step, issue.id);
300
+ }
301
+ }
302
+ ctx.ui.notify(`Created epic ${getShortId(epicId)} with ${result.issues.length} issues`, "info");
303
+ }
304
+ }
305
+
306
+ // Show plan steps with bd issue IDs
250
307
  if (todoItems.length > 0) {
251
- const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n");
308
+ const todoListText = todoItems.map((t, i) => {
309
+ const issueId = issueIds.get(t.step);
310
+ const idLabel = issueId ? `[${getShortId(issueId)}] ` : "";
311
+ return `${i + 1}. ☐ ${idLabel}${t.text}`;
312
+ }).join("\n");
313
+
314
+ const epicLabel = epicId ? `\n\nEpic: ${getShortId(epicId)}` : "";
252
315
  pi.sendMessage(
253
316
  {
254
317
  customType: "plan-todo-list",
255
- content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
318
+ content: `**Plan Steps (${todoItems.length}):**${epicLabel}\n\n${todoListText}`,
256
319
  display: true,
257
320
  },
258
321
  { triggerTurn: false },
259
322
  );
260
323
  }
261
324
 
262
- const choice = await ctx.ui.select("Plan mode - what next?", [
263
- todoItems.length > 0 ? "Execute the plan (track progress)" : "Execute the plan",
325
+ // Show "Start implementation?" prompt (no approval dialog for issue creation)
326
+ const choice = await ctx.ui.select("Start implementation?", [
327
+ todoItems.length > 0 ? "Yes, start with first step" : "Yes",
264
328
  "Stay in plan mode",
265
329
  "Refine the plan",
266
330
  ]);
267
331
 
268
- if (choice?.startsWith("Execute")) {
332
+ if (choice?.startsWith("Yes")) {
333
+ // Auto-exit plan mode and start execution
269
334
  planModeEnabled = false;
270
335
  executionMode = todoItems.length > 0;
271
336
  pi.setActiveTools(NORMAL_MODE_TOOLS);
272
337
  updateStatus(ctx);
273
338
 
339
+ // Auto-claim first issue if available
340
+ if (todoItems.length > 0 && issueIds.size > 0) {
341
+ const firstIssueId = issueIds.get(todoItems[0].step);
342
+ if (firstIssueId) {
343
+ await bdClaim(firstIssueId, cwd);
344
+ }
345
+ }
346
+
274
347
  const execMessage =
275
348
  todoItems.length > 0
276
349
  ? `Execute the plan. Start with: ${todoItems[0].text}`
@@ -298,12 +371,16 @@ After completing a step, include a [DONE:n] tag in your response.`,
298
371
  // Restore persisted state
299
372
  const planModeEntry = entries
300
373
  .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode")
301
- .pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined;
374
+ .pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean; epicId?: string; issueIds?: Record<number, string> } } | undefined;
302
375
 
303
376
  if (planModeEntry?.data) {
304
377
  planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
305
378
  todoItems = planModeEntry.data.todos ?? todoItems;
306
379
  executionMode = planModeEntry.data.executing ?? executionMode;
380
+ epicId = planModeEntry.data.epicId ?? null;
381
+ if (planModeEntry.data.issueIds) {
382
+ issueIds = new Map(Object.entries(planModeEntry.data.issueIds).map(([k, v]) => [Number(k), v]));
383
+ }
307
384
  }
308
385
 
309
386
  // On resume: re-scan messages to rebuild completion state
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@xtrm/pi-plan-mode",
3
+ "version": "1.0.0",
4
+ "description": "Plan mode extension for Pi - read-only exploration with plan extraction and bd integration",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "keywords": ["pi", "extension", "plan-mode", "xtrm"],
10
+ "author": "xtrm",
11
+ "license": "MIT"
12
+ }