bueller-wheel 0.3.0 → 0.3.1

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.
@@ -0,0 +1,92 @@
1
+ import * as fs from 'node:fs/promises';
2
+ /**
3
+ * Parses an issue markdown file and extracts the conversation history
4
+ *
5
+ * @param filePath - Absolute path to the issue file
6
+ * @returns Parsed issue with message history
7
+ * @throws Error if file cannot be read or if the format is invalid
8
+ */
9
+ export async function readIssue(filePath) {
10
+ let rawContent;
11
+ try {
12
+ rawContent = await fs.readFile(filePath, 'utf-8');
13
+ }
14
+ catch (error) {
15
+ throw new Error(`Failed to read issue file at ${filePath}: ${String(error)}`);
16
+ }
17
+ return parseIssueContent(rawContent);
18
+ }
19
+ /**
20
+ * Parses issue content and extracts the conversation history
21
+ *
22
+ * @param content - Raw markdown content of the issue file
23
+ * @returns Parsed issue with message history
24
+ */
25
+ export function parseIssueContent(content) {
26
+ const messages = [];
27
+ // Split by the separator (---)
28
+ const sections = content.split(/\n---\n/);
29
+ let messageIndex = 0;
30
+ for (const section of sections) {
31
+ const trimmedSection = section.trim();
32
+ // Skip empty sections
33
+ if (!trimmedSection) {
34
+ continue;
35
+ }
36
+ // Check if this section starts with @user: or @claude:
37
+ const userMatch = trimmedSection.match(/^@user:\s*([\s\S]*)$/);
38
+ const claudeMatch = trimmedSection.match(/^@claude:\s*([\s\S]*)$/);
39
+ if (userMatch) {
40
+ messages.push({
41
+ index: messageIndex++,
42
+ author: 'user',
43
+ content: userMatch[1].trim(),
44
+ });
45
+ }
46
+ else if (claudeMatch) {
47
+ messages.push({
48
+ index: messageIndex++,
49
+ author: 'claude',
50
+ content: claudeMatch[1].trim(),
51
+ });
52
+ }
53
+ // If no match, skip this section (handles malformed sections)
54
+ }
55
+ return {
56
+ messages,
57
+ rawContent: content,
58
+ };
59
+ }
60
+ /**
61
+ * Gets the latest message from an issue
62
+ *
63
+ * @param issue - Parsed issue object
64
+ * @returns The most recent message, or undefined if no messages exist
65
+ */
66
+ export function getLatestMessage(issue) {
67
+ if (issue.messages.length === 0) {
68
+ return undefined;
69
+ }
70
+ return issue.messages[issue.messages.length - 1];
71
+ }
72
+ /**
73
+ * Gets all messages from a specific author
74
+ *
75
+ * @param issue - Parsed issue object
76
+ * @param author - Author to filter by ('user' or 'claude')
77
+ * @returns Array of messages from the specified author
78
+ */
79
+ export function getMessagesByAuthor(issue, author) {
80
+ return issue.messages.filter((msg) => msg.author === author);
81
+ }
82
+ /**
83
+ * Formats a message for appending to an issue file
84
+ *
85
+ * @param author - Author of the message ('user' or 'claude')
86
+ * @param content - Content of the message
87
+ * @returns Formatted message string ready to append to an issue file
88
+ */
89
+ export function formatMessage(author, content) {
90
+ return `---\n\n@${author}: ${content}`;
91
+ }
92
+ //# sourceMappingURL=issue-reader.js.map
@@ -0,0 +1,236 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { parseIssueContent } from './issue-reader.js';
4
+ /**
5
+ * Searches for an issue file by filename across the issue directories
6
+ *
7
+ * @param filename - The issue filename (e.g., "p1-003-read-helper-002.md")
8
+ * @param issuesDir - Base issues directory
9
+ * @returns Located issue or null if not found
10
+ */
11
+ export async function locateIssueFile(filename, issuesDir) {
12
+ const directories = [
13
+ { dir: path.join(issuesDir, 'open'), status: 'open' },
14
+ { dir: path.join(issuesDir, 'review'), status: 'review' },
15
+ { dir: path.join(issuesDir, 'stuck'), status: 'stuck' },
16
+ ];
17
+ for (const { dir, status } of directories) {
18
+ const filePath = path.join(dir, filename);
19
+ try {
20
+ await fs.access(filePath);
21
+ return {
22
+ filePath,
23
+ status,
24
+ filename,
25
+ };
26
+ }
27
+ catch {
28
+ // File doesn't exist in this directory, continue searching
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ /**
34
+ * Resolves an issue reference (either a full path or filename) to a located issue
35
+ *
36
+ * @param reference - File path or filename
37
+ * @param issuesDir - Base issues directory
38
+ * @returns Located issue or null if not found
39
+ */
40
+ export async function resolveIssueReference(reference, issuesDir) {
41
+ // Check if it looks like a path (contains path separators)
42
+ if (reference.includes('/') || reference.includes('\\')) {
43
+ // Treat as a file path (absolute or relative)
44
+ const absolutePath = path.isAbsolute(reference) ? reference : path.resolve(reference);
45
+ try {
46
+ await fs.access(absolutePath);
47
+ // Determine status from path
48
+ let status = 'open';
49
+ if (absolutePath.includes('/review/')) {
50
+ status = 'review';
51
+ }
52
+ else if (absolutePath.includes('/stuck/')) {
53
+ status = 'stuck';
54
+ }
55
+ return {
56
+ filePath: absolutePath,
57
+ status,
58
+ filename: path.basename(absolutePath),
59
+ };
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ // Otherwise, treat it as a filename and search for it
66
+ return locateIssueFile(reference, issuesDir);
67
+ }
68
+ /**
69
+ * Abbreviates a message based on its position in the conversation
70
+ *
71
+ * @param message - The message to abbreviate
72
+ * @param _position - Position in conversation ('first', 'middle', or 'last')
73
+ * @param maxLength - Maximum length for the abbreviated content
74
+ * @returns Abbreviated message
75
+ */
76
+ function abbreviateMessage(message, _position, maxLength) {
77
+ const fullContent = message.content;
78
+ let abbreviated = fullContent;
79
+ let isAbbreviated = false;
80
+ if (fullContent.length > maxLength) {
81
+ abbreviated = fullContent.substring(0, maxLength).trimEnd() + '…';
82
+ isAbbreviated = true;
83
+ }
84
+ return {
85
+ index: message.index,
86
+ author: message.author,
87
+ content: abbreviated,
88
+ isAbbreviated,
89
+ fullContent,
90
+ };
91
+ }
92
+ /**
93
+ * Creates abbreviated messages from a parsed issue
94
+ *
95
+ * @param messages - Array of issue messages
96
+ * @returns Array of abbreviated messages
97
+ */
98
+ function createAbbreviatedMessages(messages) {
99
+ if (messages.length === 0) {
100
+ return [];
101
+ }
102
+ if (messages.length === 1) {
103
+ // Single message - use 230 char limit
104
+ return [abbreviateMessage(messages[0], 'first', 230)];
105
+ }
106
+ const result = [];
107
+ // First message - 230 chars
108
+ result.push(abbreviateMessage(messages[0], 'first', 230));
109
+ // Middle messages - 70 chars
110
+ for (let i = 1; i < messages.length - 1; i++) {
111
+ result.push(abbreviateMessage(messages[i], 'middle', 70));
112
+ }
113
+ // Last message - 230 chars
114
+ result.push(abbreviateMessage(messages[messages.length - 1], 'last', 230));
115
+ return result;
116
+ }
117
+ /**
118
+ * Summarizes an issue file with abbreviated messages
119
+ *
120
+ * @param locatedIssue - Located issue information
121
+ * @returns Issue summary
122
+ */
123
+ export async function summarizeIssue(locatedIssue) {
124
+ const content = await fs.readFile(locatedIssue.filePath, 'utf-8');
125
+ const parsed = parseIssueContent(content);
126
+ const abbreviatedMessages = createAbbreviatedMessages(parsed.messages);
127
+ return {
128
+ issue: locatedIssue,
129
+ abbreviatedMessages,
130
+ messageCount: parsed.messages.length,
131
+ };
132
+ }
133
+ /**
134
+ * Parses index specification (e.g., "3" or "1,3")
135
+ *
136
+ * @param indexSpec - Index specification string
137
+ * @returns Object with indices array and whether it's a single index, or null if invalid
138
+ */
139
+ export function parseIndexSpec(indexSpec) {
140
+ const parts = indexSpec.split(',').map((s) => s.trim());
141
+ if (parts.length === 1) {
142
+ // Single index
143
+ const index = parseInt(parts[0], 10);
144
+ if (isNaN(index) || index < 0) {
145
+ return null;
146
+ }
147
+ return { indices: [index], isSingleIndex: true };
148
+ }
149
+ if (parts.length === 2) {
150
+ // Range
151
+ const start = parseInt(parts[0], 10);
152
+ const end = parseInt(parts[1], 10);
153
+ if (isNaN(start) || isNaN(end) || start < 0 || end < start) {
154
+ return null;
155
+ }
156
+ const indices = [];
157
+ for (let i = start; i <= end; i++) {
158
+ indices.push(i);
159
+ }
160
+ return { indices, isSingleIndex: false };
161
+ }
162
+ return null;
163
+ }
164
+ /**
165
+ * Expands specific messages in a summary based on index specification
166
+ *
167
+ * @param summary - Issue summary
168
+ * @param indexSpec - Index specification (e.g., "3" or "1,3")
169
+ * @returns New summary with expanded messages and filter info
170
+ */
171
+ export function expandMessages(summary, indexSpec) {
172
+ const parsed = parseIndexSpec(indexSpec);
173
+ if (!parsed) {
174
+ return summary;
175
+ }
176
+ const { indices, isSingleIndex } = parsed;
177
+ const expandedMessages = summary.abbreviatedMessages.map((msg) => {
178
+ if (indices.includes(msg.index)) {
179
+ return {
180
+ ...msg,
181
+ content: msg.fullContent,
182
+ isAbbreviated: false,
183
+ };
184
+ }
185
+ return msg;
186
+ });
187
+ return {
188
+ ...summary,
189
+ abbreviatedMessages: expandedMessages,
190
+ filterToIndices: indices,
191
+ isSingleIndex,
192
+ };
193
+ }
194
+ /**
195
+ * Condenses text by trimming lines and replacing newlines with single spaces
196
+ *
197
+ * @param text - Text to condense
198
+ * @returns Condensed text
199
+ */
200
+ function condenseText(text) {
201
+ return text
202
+ .split('\n')
203
+ .map((line) => line.trim())
204
+ .filter((line) => line.length > 0)
205
+ .join(' ');
206
+ }
207
+ /**
208
+ * Formats an issue summary for console output
209
+ *
210
+ * @param summary - Issue summary (may include filterToIndices and isSingleIndex)
211
+ * @param indexSpec - Optional index specification for filtering display
212
+ * @returns Formatted string
213
+ */
214
+ export function formatIssueSummary(summary, indexSpec) {
215
+ const lines = [];
216
+ // Header
217
+ const directory = `${summary.issue.status}/`;
218
+ const filename = summary.issue.filename;
219
+ lines.push(`${directory}${filename}`);
220
+ // Determine which messages to show
221
+ const messagesToShow = summary.filterToIndices
222
+ ? summary.abbreviatedMessages.filter((msg) => summary.filterToIndices.includes(msg.index))
223
+ : summary.abbreviatedMessages;
224
+ // Messages
225
+ for (const msg of messagesToShow) {
226
+ const content = msg.isAbbreviated ? condenseText(msg.content) : msg.content;
227
+ lines.push(`[${msg.index}] @${msg.author}: ${content}`);
228
+ }
229
+ // Add follow-up action hint if not showing specific indices
230
+ if (!indexSpec || !summary.isSingleIndex) {
231
+ lines.push('');
232
+ lines.push('Pass `--index N` or `--index M,N` to see more.');
233
+ }
234
+ return lines.join('\n');
235
+ }
236
+ //# sourceMappingURL=issue-summarize.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bueller-wheel",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Headless Claude Code issue processor - A wrapper that runs Claude Code in a loop to process issues from a directory queue",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,7 @@
8
8
  "bueller-wheel": "dist/index.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "rm -rf dist out && tsc && mkdir -p dist && cp out/src/index.js dist/index.js && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp && mv dist/index.tmp dist/index.js && chmod +x dist/index.js",
11
+ "build": "rm -rf dist out && tsc && mkdir -p dist && cp out/src/*.js dist/ && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp && mv dist/index.tmp dist/index.js && chmod +x dist/index.js",
12
12
  "dev": "tsx src/index.ts",
13
13
  "start": "node dist/index.js",
14
14
  "test": "tsx tests/test-runner.ts",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "homepage": "https://github.com/fongandrew/bueller-wheel#readme",
39
39
  "files": [
40
- "dist/index.js",
40
+ "dist/*.js",
41
41
  "README.md",
42
42
  "LICENSE"
43
43
  ],