@link-assistant/hive-mind 0.41.2 → 0.41.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.41.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 27bbc44: Add backslash detection and validation in GitHub URLs
8
+
9
+ When users provide URLs with backslashes (e.g., `https://github.com/owner/repo/issues/123\`), the system now properly validates them and provides helpful error messages with auto-corrected URL suggestions. According to RFC 3986, backslash is not a valid character in URL paths.
10
+
11
+ **Changes:**
12
+
13
+ - Enhanced `parseGitHubUrl()` function to detect backslashes in URL paths
14
+ - Updated all validation points (Telegram bot `/solve` and `/hive` commands, CLI `hive` and `solve` commands)
15
+ - Provides user-friendly error messages with corrected URL suggestions
16
+ - Comprehensive test suite for backslash validation scenarios
17
+
18
+ Fixes #923
19
+
20
+ ## 0.41.3
21
+
22
+ ### Patch Changes
23
+
24
+ - db8cef7: Fix CLAUDE.md not being deleted in continue mode
25
+
26
+ When a work session completes successfully but the CLAUDE.md commit hash was lost between sessions (e.g., due to session interruption), the system now attempts to detect the CLAUDE.md commit from the branch structure instead of silently skipping cleanup.
27
+
28
+ **Safety Checks (Preventing Issue #617 Recurrence):**
29
+
30
+ 1. CLAUDE.md must exist in current branch
31
+ 2. Find merge base to isolate PR-only commits
32
+ 3. Must have at least 2 commits (CLAUDE.md + actual work)
33
+ 4. First commit message must match expected pattern
34
+ 5. First commit must ONLY change CLAUDE.md file
35
+
36
+ Fixes #940
37
+
3
38
  ## 0.41.2
4
39
 
5
40
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.41.2",
3
+ "version": "0.41.5",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -1107,6 +1107,22 @@ export function parseGitHubUrl(url) {
1107
1107
  if (normalizedUrl.startsWith('http://')) {
1108
1108
  normalizedUrl = normalizedUrl.replace(/^http:\/\//, 'https://');
1109
1109
  }
1110
+
1111
+ // Check for backslashes in the URL path (excluding query params and hash)
1112
+ // According to RFC 3986, backslash is not a valid character in URL paths
1113
+ const urlBeforeQueryAndHash = normalizedUrl.split('?')[0].split('#')[0];
1114
+ if (urlBeforeQueryAndHash.includes('\\')) {
1115
+ // Generate suggested URL by replacing backslashes with forward slashes
1116
+ const suggestedUrl = urlBeforeQueryAndHash.replace(/\\/g, '/');
1117
+ const urlAfterPath = normalizedUrl.substring(urlBeforeQueryAndHash.length);
1118
+
1119
+ return {
1120
+ valid: false,
1121
+ error: 'Invalid character in URL: backslash (\\) is not allowed in URL paths',
1122
+ suggestion: suggestedUrl + urlAfterPath
1123
+ };
1124
+ }
1125
+
1110
1126
  // Parse the URL
1111
1127
  let urlObj;
1112
1128
  try {
package/src/hive.mjs CHANGED
@@ -337,10 +337,9 @@ if (githubUrl) {
337
337
 
338
338
  if (!parsedUrl.valid) {
339
339
  console.error('Error: Invalid GitHub URL format');
340
- if (parsedUrl.error) {
341
- console.error(` ${parsedUrl.error}`);
342
- }
343
- console.error('Expected: https://github.com/owner or https://github.com/owner/repo');
340
+ if (parsedUrl.error) console.error(` ${parsedUrl.error}`);
341
+ if (parsedUrl.suggestion) console.error(`\nšŸ’” Did you mean: ${parsedUrl.suggestion}`);
342
+ console.error('\nExpected: https://github.com/owner or https://github.com/owner/repo');
344
343
  console.error('You can use any of these formats:');
345
344
  console.error(' - https://github.com/owner');
346
345
  console.error(' - https://github.com/owner/repo');
@@ -50,14 +50,151 @@ const { reportError } = sentryLib;
50
50
  const githubLinking = await import('./github-linking.lib.mjs');
51
51
  const { hasGitHubLinkingKeyword } = githubLinking;
52
52
 
53
+ /**
54
+ * Detect the CLAUDE.md commit hash from branch structure when not available in session
55
+ * This handles continue mode where the commit hash was lost between sessions
56
+ *
57
+ * Safety checks to prevent Issue #617 (wrong commit revert):
58
+ * 1. Only look at commits on the PR branch (not default branch commits)
59
+ * 2. Verify the commit message matches our expected pattern
60
+ * 3. Verify the commit ONLY adds CLAUDE.md (no other files changed)
61
+ * 4. Verify there are additional commits after it (actual work was done)
62
+ *
63
+ * @param {string} tempDir - The temporary directory with the git repo
64
+ * @param {string} branchName - The PR branch name
65
+ * @returns {string|null} - The detected commit hash or null if not found/safe
66
+ */
67
+ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
68
+ try {
69
+ await log(' Attempting to detect CLAUDE.md commit from branch structure...', { verbose: true });
70
+
71
+ // First check if CLAUDE.md exists in current branch
72
+ const claudeMdExistsResult = await $({ cwd: tempDir })`git ls-files CLAUDE.md 2>&1`;
73
+ if (claudeMdExistsResult.code !== 0 || !claudeMdExistsResult.stdout || !claudeMdExistsResult.stdout.trim()) {
74
+ await log(' CLAUDE.md does not exist in current branch', { verbose: true });
75
+ return null;
76
+ }
77
+
78
+ // Get the default branch to find the fork point
79
+ const defaultBranchResult = await $({ cwd: tempDir })`git symbolic-ref refs/remotes/origin/HEAD 2>&1`;
80
+ let defaultBranch = 'main';
81
+ if (defaultBranchResult.code === 0 && defaultBranchResult.stdout) {
82
+ const match = defaultBranchResult.stdout.toString().match(/refs\/remotes\/origin\/(.+)/);
83
+ if (match) {
84
+ defaultBranch = match[1].trim();
85
+ }
86
+ }
87
+ await log(` Using default branch: ${defaultBranch}`, { verbose: true });
88
+
89
+ // Find the merge base (fork point) between current branch and default branch
90
+ const mergeBaseResult = await $({ cwd: tempDir })`git merge-base origin/${defaultBranch} HEAD 2>&1`;
91
+ if (mergeBaseResult.code !== 0 || !mergeBaseResult.stdout) {
92
+ await log(' Could not find merge base, cannot safely detect CLAUDE.md commit', { verbose: true });
93
+ return null;
94
+ }
95
+ const mergeBase = mergeBaseResult.stdout.toString().trim();
96
+ await log(` Merge base: ${mergeBase.substring(0, 7)}`, { verbose: true });
97
+
98
+ // Get all commits on the PR branch (commits after the merge base)
99
+ // Format: hash|message|files_changed
100
+ const branchCommitsResult = await $({ cwd: tempDir })`git log ${mergeBase}..HEAD --reverse --format="%H|%s" 2>&1`;
101
+ if (branchCommitsResult.code !== 0 || !branchCommitsResult.stdout) {
102
+ await log(' No commits found on PR branch', { verbose: true });
103
+ return null;
104
+ }
105
+
106
+ const branchCommits = branchCommitsResult.stdout.toString().trim().split('\n').filter(Boolean);
107
+ if (branchCommits.length === 0) {
108
+ await log(' No commits found on PR branch', { verbose: true });
109
+ return null;
110
+ }
111
+
112
+ await log(` Found ${branchCommits.length} commit(s) on PR branch`, { verbose: true });
113
+
114
+ // Safety check: Must have at least 2 commits (CLAUDE.md commit + actual work)
115
+ if (branchCommits.length < 2) {
116
+ await log(' Only 1 commit on branch - not enough commits to safely revert CLAUDE.md', { verbose: true });
117
+ await log(' (Need at least 2 commits: CLAUDE.md initial + actual work)', { verbose: true });
118
+ return null;
119
+ }
120
+
121
+ // Get the first commit on the PR branch
122
+ const firstCommitLine = branchCommits[0];
123
+ const [firstCommitHash, firstCommitMessage] = firstCommitLine.split('|');
124
+
125
+ await log(` First commit on branch: ${firstCommitHash.substring(0, 7)} - "${firstCommitMessage}"`, { verbose: true });
126
+
127
+ // Safety check: Verify commit message matches expected pattern
128
+ const expectedMessagePatterns = [
129
+ /^Initial commit with task details/i,
130
+ /^Add CLAUDE\.md/i,
131
+ /^CLAUDE\.md/i
132
+ ];
133
+
134
+ const messageMatches = expectedMessagePatterns.some(pattern => pattern.test(firstCommitMessage));
135
+ if (!messageMatches) {
136
+ await log(' First commit message does not match expected CLAUDE.md pattern', { verbose: true });
137
+ await log(' Expected patterns: "Initial commit with task details...", "Add CLAUDE.md", etc.', { verbose: true });
138
+ return null;
139
+ }
140
+
141
+ // Safety check: Verify the commit ONLY adds CLAUDE.md file (no other files)
142
+ const filesChangedResult = await $({ cwd: tempDir })`git diff-tree --no-commit-id --name-only -r ${firstCommitHash} 2>&1`;
143
+ if (filesChangedResult.code !== 0 || !filesChangedResult.stdout) {
144
+ await log(' Could not get files changed in first commit', { verbose: true });
145
+ return null;
146
+ }
147
+
148
+ const filesChanged = filesChangedResult.stdout.toString().trim().split('\n').filter(Boolean);
149
+ await log(` Files changed in first commit: ${filesChanged.join(', ')}`, { verbose: true });
150
+
151
+ // Check if CLAUDE.md is in the files changed
152
+ if (!filesChanged.includes('CLAUDE.md')) {
153
+ await log(' First commit does not include CLAUDE.md', { verbose: true });
154
+ return null;
155
+ }
156
+
157
+ // CRITICAL SAFETY CHECK: Only allow revert if CLAUDE.md is the ONLY file changed
158
+ // This prevents Issue #617 where reverting a commit deleted .gitignore, LICENSE, README.md
159
+ if (filesChanged.length > 1) {
160
+ await log(` āš ļø First commit changes more than just CLAUDE.md (${filesChanged.length} files)`, { verbose: true });
161
+ await log(` Files: ${filesChanged.join(', ')}`, { verbose: true });
162
+ await log(' Refusing to revert to prevent data loss (Issue #617 safety)', { verbose: true });
163
+ return null;
164
+ }
165
+
166
+ // All safety checks passed!
167
+ await log(` āœ… Detected CLAUDE.md commit: ${firstCommitHash.substring(0, 7)}`, { verbose: true });
168
+ await log(' āœ… Commit only contains CLAUDE.md (safe to revert)', { verbose: true });
169
+ await log(` āœ… Branch has ${branchCommits.length - 1} additional commit(s) (work was done)`, { verbose: true });
170
+
171
+ return firstCommitHash;
172
+ } catch (error) {
173
+ reportError(error, {
174
+ context: 'detect_claude_md_commit',
175
+ tempDir,
176
+ branchName,
177
+ operation: 'detect_commit_from_branch_structure'
178
+ });
179
+ await log(` Error detecting CLAUDE.md commit: ${error.message}`, { verbose: true });
180
+ return null;
181
+ }
182
+ };
183
+
53
184
  // Revert the CLAUDE.md commit to restore original state
54
185
  export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash = null) => {
55
186
  try {
56
- // Only revert if we have the commit hash from this session
57
- // This prevents reverting the wrong commit in continue mode
187
+ // If no commit hash provided, try to detect it from branch structure
188
+ // This handles continue mode where the hash was lost between sessions
58
189
  if (!claudeCommitHash) {
59
- await log(' No CLAUDE.md commit to revert (not created in this session)', { verbose: true });
60
- return;
190
+ await log(' No CLAUDE.md commit hash from session, attempting to detect from branch...', { verbose: true });
191
+ claudeCommitHash = await detectClaudeMdCommitFromBranch(tempDir, branchName);
192
+
193
+ if (!claudeCommitHash) {
194
+ await log(' Could not safely detect CLAUDE.md commit to revert', { verbose: true });
195
+ return;
196
+ }
197
+ await log(` Detected CLAUDE.md commit: ${claudeCommitHash.substring(0, 7)}`, { verbose: true });
61
198
  }
62
199
 
63
200
  await log(formatAligned('šŸ”„', 'Cleanup:', 'Reverting CLAUDE.md commit'));
@@ -66,10 +66,9 @@ export const validateGitHubUrl = (issueUrl) => {
66
66
 
67
67
  if (!parsedUrl.valid) {
68
68
  console.error('Error: Invalid GitHub URL format');
69
- if (parsedUrl.error) {
70
- console.error(` ${parsedUrl.error}`);
71
- }
72
- console.error(' Please provide a valid GitHub issue or pull request URL');
69
+ if (parsedUrl.error) console.error(` ${parsedUrl.error}`);
70
+ if (parsedUrl.suggestion) console.error(`\nšŸ’” Did you mean: ${parsedUrl.suggestion}`);
71
+ console.error('\n Please provide a valid GitHub issue or pull request URL');
73
72
  console.error(' Examples:');
74
73
  console.error(' https://github.com/owner/repo/issues/123 (issue)');
75
74
  console.error(' https://github.com/owner/repo/pull/456 (pull request)');
@@ -638,7 +638,8 @@ function validateGitHubUrl(args, options = {}) {
638
638
  if (!parsed.valid) {
639
639
  return {
640
640
  valid: false,
641
- error: parsed.error || 'Invalid GitHub URL'
641
+ error: parsed.error || 'Invalid GitHub URL',
642
+ suggestion: parsed.suggestion
642
643
  };
643
644
  }
644
645
 
@@ -987,7 +988,12 @@ bot.command(/^solve$/i, async (ctx) => {
987
988
 
988
989
  const validation = validateGitHubUrl(userArgs);
989
990
  if (!validation.valid) {
990
- await ctx.reply(`āŒ ${validation.error}\n\nExample: \`/solve https://github.com/owner/repo/issues/123\`\n\nOr reply to a message containing a GitHub link with \`/solve\``, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
991
+ let errorMsg = `āŒ ${validation.error}`;
992
+ if (validation.suggestion) {
993
+ errorMsg += `\n\nšŸ’” Did you mean: \`${validation.suggestion}\``;
994
+ }
995
+ errorMsg += '\n\nExample: `/solve https://github.com/owner/repo/issues/123`\n\nOr reply to a message containing a GitHub link with `/solve`';
996
+ await ctx.reply(errorMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
991
997
  return;
992
998
  }
993
999
 
@@ -1136,7 +1142,12 @@ bot.command(/^hive$/i, async (ctx) => {
1136
1142
  exampleUrl: 'https://github.com/owner/repo'
1137
1143
  });
1138
1144
  if (!validation.valid) {
1139
- await ctx.reply(`āŒ ${validation.error}\n\nExample: \`/hive https://github.com/owner/repo\``, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1145
+ let errorMsg = `āŒ ${validation.error}`;
1146
+ if (validation.suggestion) {
1147
+ errorMsg += `\n\nšŸ’” Did you mean: \`${validation.suggestion}\``;
1148
+ }
1149
+ errorMsg += '\n\nExample: `/hive https://github.com/owner/repo`';
1150
+ await ctx.reply(errorMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1140
1151
  return;
1141
1152
  }
1142
1153