@link-assistant/hive-mind 1.73.9 → 1.74.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.
- package/CHANGELOG.md +51 -0
- package/README.hi.md +36 -0
- package/README.md +37 -0
- package/README.ru.md +37 -0
- package/README.zh.md +35 -0
- package/package.json +2 -1
- package/src/claude.lib.mjs +2 -0
- package/src/cleanup.lib.mjs +359 -0
- package/src/cleanup.mjs +288 -0
- package/src/cleanup.os.lib.mjs +404 -0
- package/src/codex.lib.mjs +2 -0
- package/src/interactive-image-render.lib.mjs +140 -0
- package/src/interactive-image-upload.lib.mjs +415 -0
- package/src/interactive-mode.lib.mjs +27 -8
- package/src/interactive-mode.shared.lib.mjs +97 -0
- package/src/solve.config.lib.mjs +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.74.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 59eee9a: feat(interactive-mode): display images the AI reads/writes inline in PR comments (#1843)
|
|
8
|
+
|
|
9
|
+
When `--interactive-mode` posts Claude/Codex tool activity as PR comments, any
|
|
10
|
+
images the AI reads or produces (the `Read` tool on a screenshot, Playwright
|
|
11
|
+
captures, MCP image results) were previously serialized as multi-kilobyte
|
|
12
|
+
base64 blobs inside the "Raw JSON" section — unreadable and pushing comments
|
|
13
|
+
toward GitHub's size limit.
|
|
14
|
+
|
|
15
|
+
Those base64 payloads are now uploaded to hidden custom Git refs
|
|
16
|
+
(`refs/hive-mind-media/pr-...`) via the Git Data API and embedded inline in the
|
|
17
|
+
comment as commit-SHA `` blob URLs, so reviewers see the actual
|
|
18
|
+
image (GitHub's Camo proxy renders `?raw=true` blob URLs inline for both public
|
|
19
|
+
and private repos, whereas `data:` URIs are stripped by the comment sanitizer).
|
|
20
|
+
Uploads are content-hashed (SHA-256) for dedup, and the base64 is redacted from
|
|
21
|
+
the Raw JSON section with a `<image data: N base64 chars>` placeholder.
|
|
22
|
+
|
|
23
|
+
Enabled by default; use `--no-interactive-image-upload` to opt out, in which
|
|
24
|
+
case each image degrades to a compact metadata note instead of being embedded.
|
|
25
|
+
All comment bodies continue to pass through the token sanitizer (#1745).
|
|
26
|
+
|
|
27
|
+
## 1.74.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- b00a51c: feat(cleanup): add a task-aware `cleanup` command to free disk space safely (#1848)
|
|
32
|
+
|
|
33
|
+
Adds a new `cleanup` bin that removes stale hive-mind temporary
|
|
34
|
+
directories/files under the system temp dir while preserving folders that belong
|
|
35
|
+
to currently-running tasks, protected system paths, and any clone with
|
|
36
|
+
uncommitted or unpushed work.
|
|
37
|
+
|
|
38
|
+
Highlights:
|
|
39
|
+
- `--dry-run` / `-n` prints the full list of kept folders and folders that would
|
|
40
|
+
be deleted (with sizes and reasons), deleting nothing.
|
|
41
|
+
- `--keep-active-tasks-folders` (default on) detects active tasks from running
|
|
42
|
+
processes (`/proc`) and live isolation sessions (`screen`/`tmux` +
|
|
43
|
+
`$ --status`), and matches clones to tasks by branch name using the same logic
|
|
44
|
+
as `solve` (issue → `issue-{n}-{hex}` scoped to the repo; PR → its resolved
|
|
45
|
+
head branch). Disable with `--no-keep-active-tasks-folders`.
|
|
46
|
+
- Keeps `/tmp/start-command/` and system-owned temp entries by default;
|
|
47
|
+
`--force-start-command` allows deleting `/tmp/start-command` when needed.
|
|
48
|
+
- Optional Ubuntu/system cleanup behind explicit flags: `--apt`, `--journal`,
|
|
49
|
+
`--docker`, `--npm` (and `--system` shorthand), with `--sudo`.
|
|
50
|
+
- Safe by default: keeps unrecognised entries unless `--all`, never deletes
|
|
51
|
+
paths held open by a running process or used by the cleanup process itself,
|
|
52
|
+
and requires confirmation unless `--force`.
|
|
53
|
+
|
|
3
54
|
## 1.73.9
|
|
4
55
|
|
|
5
56
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -717,6 +717,42 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
|
|
|
717
717
|
(cd /tmp/gh-issue-solver-123456789 && claude --resume session-id)
|
|
718
718
|
```
|
|
719
719
|
|
|
720
|
+
### डिस्क क्लीनअप
|
|
721
|
+
|
|
722
|
+
`cleanup` पुरानी hive-mind अस्थायी डायरेक्टरी/फ़ाइलों (जैसे प्रति-कार्य क्लोन
|
|
723
|
+
`/tmp/gh-issue-solver-*`, MCP कॉन्फ़िग फ़ाइलें, लॉग डाउनलोड डायरेक्टरी आदि) को हटाकर
|
|
724
|
+
डिस्क स्थान खाली करता है, जबकि **वर्तमान में चल रहे कार्यों से संबंधित फ़ोल्डर**,
|
|
725
|
+
सुरक्षित सिस्टम पथ, और बिना कमिट या बिना पुश किए बदलावों वाले किसी भी क्लोन को बनाए
|
|
726
|
+
रखता है। यह चल रही प्रक्रियाओं और लाइव आइसोलेशन सेशन से सक्रिय कार्यों का पता लगाता है
|
|
727
|
+
और `solve` के समान लॉजिक का उपयोग करते हुए शाखा नाम द्वारा क्लोन को कार्यों से मिलाता
|
|
728
|
+
है (issue → `issue-{n}-{hex}`; PR → इसकी हल की गई head शाखा)।
|
|
729
|
+
|
|
730
|
+
```bash
|
|
731
|
+
# पूर्वावलोकन: रखे जाने वाले और हटाए जाने वाले फ़ोल्डरों की सूची (कुछ भी नहीं हटाता)
|
|
732
|
+
cleanup --dry-run
|
|
733
|
+
|
|
734
|
+
# पुरानी अस्थायी फ़ाइलें वास्तव में हटाएँ (पहले पुष्टि माँगता है)
|
|
735
|
+
cleanup
|
|
736
|
+
|
|
737
|
+
# पुष्टि प्रॉम्प्ट के बिना हटाएँ
|
|
738
|
+
cleanup --force
|
|
739
|
+
|
|
740
|
+
# गैर-hive-mind अस्थायी प्रविष्टियों पर भी विचार करें (अधिक आक्रामक)
|
|
741
|
+
cleanup --all --dry-run
|
|
742
|
+
|
|
743
|
+
# /tmp/start-command को हटाने की अनुमति दें (डिफ़ॉल्ट रूप से रखा जाता है; इसमें आइसोलेशन लॉग होते हैं)
|
|
744
|
+
cleanup --force-start-command
|
|
745
|
+
|
|
746
|
+
# Ubuntu / सिस्टम क्लीनअप (apt कैश, journald लॉग, npm कैश)
|
|
747
|
+
cleanup --system --sudo
|
|
748
|
+
|
|
749
|
+
# सक्रिय-कार्य पहचान अक्षम करें (केवल सुरक्षित पथ रखे जाते हैं)
|
|
750
|
+
cleanup --no-keep-active-tasks-folders --dry-run
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
विकल्पों की पूरी सूची के लिए `cleanup --help` चलाएँ। यह कमांड dry-run के अनुकूल है और
|
|
754
|
+
हर रन के लिए टाइमस्टैम्प वाला `cleanup-*.log` लिखता है।
|
|
755
|
+
|
|
720
756
|
## 🔍 निगरानी और लॉगिंग
|
|
721
757
|
|
|
722
758
|
लॉग में resume कमांड खोजें:
|
package/README.md
CHANGED
|
@@ -737,6 +737,43 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
|
|
|
737
737
|
(cd /tmp/gh-issue-solver-123456789 && claude --resume session-id)
|
|
738
738
|
```
|
|
739
739
|
|
|
740
|
+
### Disk Cleanup
|
|
741
|
+
|
|
742
|
+
`cleanup` frees disk space by removing stale hive-mind temporary
|
|
743
|
+
directories/files (per-task clones like `/tmp/gh-issue-solver-*`, MCP config
|
|
744
|
+
files, log download dirs, …) while **keeping folders that belong to
|
|
745
|
+
currently-running tasks**, protected system paths, and any clone with
|
|
746
|
+
uncommitted or unpushed work. It detects active tasks from running processes and
|
|
747
|
+
live isolation sessions and matches clones to tasks by branch name using the
|
|
748
|
+
same logic as `solve` (issue → `issue-{n}-{hex}`; PR → its resolved head
|
|
749
|
+
branch).
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
# Preview: list kept folders and folders that would be deleted (deletes nothing)
|
|
753
|
+
cleanup --dry-run
|
|
754
|
+
|
|
755
|
+
# Actually delete stale temp artifacts (asks for confirmation first)
|
|
756
|
+
cleanup
|
|
757
|
+
|
|
758
|
+
# Delete without the confirmation prompt
|
|
759
|
+
cleanup --force
|
|
760
|
+
|
|
761
|
+
# Also consider non-hive-mind temp entries (more aggressive)
|
|
762
|
+
cleanup --all --dry-run
|
|
763
|
+
|
|
764
|
+
# Allow deleting /tmp/start-command (kept by default; holds isolation logs)
|
|
765
|
+
cleanup --force-start-command
|
|
766
|
+
|
|
767
|
+
# Ubuntu / system cleanup (apt caches, journald logs, npm cache)
|
|
768
|
+
cleanup --system --sudo
|
|
769
|
+
|
|
770
|
+
# Disable active-task detection (only protected paths are kept)
|
|
771
|
+
cleanup --no-keep-active-tasks-folders --dry-run
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
Run `cleanup --help` for the full list of options. The command is dry-run
|
|
775
|
+
friendly and writes a timestamped `cleanup-*.log` for every run.
|
|
776
|
+
|
|
740
777
|
## 🔍 Monitoring & Logging
|
|
741
778
|
|
|
742
779
|
Find resume commands in logs:
|
package/README.ru.md
CHANGED
|
@@ -719,6 +719,43 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
|
|
|
719
719
|
(cd /tmp/gh-issue-solver-123456789 && claude --resume session-id)
|
|
720
720
|
```
|
|
721
721
|
|
|
722
|
+
### Очистка диска
|
|
723
|
+
|
|
724
|
+
`cleanup` освобождает место на диске, удаляя устаревшие временные каталоги/файлы
|
|
725
|
+
hive-mind (клоны для каждой задачи вида `/tmp/gh-issue-solver-*`, файлы
|
|
726
|
+
конфигурации MCP, каталоги загрузки логов и т. д.), при этом **сохраняя папки,
|
|
727
|
+
относящиеся к выполняющимся в данный момент задачам**, защищённые системные пути и
|
|
728
|
+
любой клон с незакоммиченными или неотправленными изменениями. Он обнаруживает
|
|
729
|
+
активные задачи по запущенным процессам и активным сессиям изоляции и сопоставляет
|
|
730
|
+
клоны с задачами по имени ветки, используя ту же логику, что и `solve`
|
|
731
|
+
(issue → `issue-{n}-{hex}`; PR → его разрешённая head-ветка).
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
# Предпросмотр: список сохраняемых и удаляемых папок (ничего не удаляет)
|
|
735
|
+
cleanup --dry-run
|
|
736
|
+
|
|
737
|
+
# Реально удалить устаревшие временные файлы (сначала запросит подтверждение)
|
|
738
|
+
cleanup
|
|
739
|
+
|
|
740
|
+
# Удалить без запроса подтверждения
|
|
741
|
+
cleanup --force
|
|
742
|
+
|
|
743
|
+
# Учитывать также не-hive-mind временные записи (более агрессивно)
|
|
744
|
+
cleanup --all --dry-run
|
|
745
|
+
|
|
746
|
+
# Разрешить удаление /tmp/start-command (по умолчанию сохраняется; хранит логи изоляции)
|
|
747
|
+
cleanup --force-start-command
|
|
748
|
+
|
|
749
|
+
# Очистка Ubuntu / системы (кэши apt, логи journald, кэш npm)
|
|
750
|
+
cleanup --system --sudo
|
|
751
|
+
|
|
752
|
+
# Отключить обнаружение активных задач (сохраняются только защищённые пути)
|
|
753
|
+
cleanup --no-keep-active-tasks-folders --dry-run
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
Запустите `cleanup --help`, чтобы увидеть полный список опций. Команда удобна для
|
|
757
|
+
режима dry-run и записывает лог `cleanup-*.log` с меткой времени при каждом запуске.
|
|
758
|
+
|
|
722
759
|
## 🔍 Мониторинг и логирование
|
|
723
760
|
|
|
724
761
|
Найдите команды возобновления в логах:
|
package/README.zh.md
CHANGED
|
@@ -713,6 +713,41 @@ solve https://github.com/owner/repo/issues/123 --resume 657e6db1-6eb3-4a8d
|
|
|
713
713
|
(cd /tmp/gh-issue-solver-123456789 && claude --resume session-id)
|
|
714
714
|
```
|
|
715
715
|
|
|
716
|
+
### 磁盘清理
|
|
717
|
+
|
|
718
|
+
`cleanup` 通过删除过时的 hive-mind 临时目录/文件(如每个任务的克隆
|
|
719
|
+
`/tmp/gh-issue-solver-*`、MCP 配置文件、日志下载目录等)来释放磁盘空间,同时
|
|
720
|
+
**保留属于当前正在运行任务的文件夹**、受保护的系统路径,以及任何包含未提交或未推送
|
|
721
|
+
更改的克隆。它通过运行中的进程和实时隔离会话检测活动任务,并使用与 `solve` 相同的
|
|
722
|
+
逻辑按分支名称将克隆与任务匹配(issue → `issue-{n}-{hex}`;PR → 其解析出的 head
|
|
723
|
+
分支)。
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
# 预览:列出保留的文件夹和将被删除的文件夹(不删除任何内容)
|
|
727
|
+
cleanup --dry-run
|
|
728
|
+
|
|
729
|
+
# 实际删除过时的临时文件(会先要求确认)
|
|
730
|
+
cleanup
|
|
731
|
+
|
|
732
|
+
# 删除时不显示确认提示
|
|
733
|
+
cleanup --force
|
|
734
|
+
|
|
735
|
+
# 同时考虑非 hive-mind 临时项(更激进)
|
|
736
|
+
cleanup --all --dry-run
|
|
737
|
+
|
|
738
|
+
# 允许删除 /tmp/start-command(默认保留;其中存放隔离日志)
|
|
739
|
+
cleanup --force-start-command
|
|
740
|
+
|
|
741
|
+
# Ubuntu / 系统清理(apt 缓存、journald 日志、npm 缓存)
|
|
742
|
+
cleanup --system --sudo
|
|
743
|
+
|
|
744
|
+
# 禁用活动任务检测(仅保留受保护的路径)
|
|
745
|
+
cleanup --no-keep-active-tasks-folders --dry-run
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
运行 `cleanup --help` 查看完整的选项列表。该命令对 dry-run 友好,并为每次运行写入
|
|
749
|
+
带时间戳的 `cleanup-*.log` 日志。
|
|
750
|
+
|
|
716
751
|
## 🔍 监控与日志
|
|
717
752
|
|
|
718
753
|
在日志中查找恢复命令:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.1",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"hive": "./src/hive.mjs",
|
|
9
9
|
"solve": "./src/solve.mjs",
|
|
10
10
|
"task": "./src/task.mjs",
|
|
11
|
+
"cleanup": "./src/cleanup.mjs",
|
|
11
12
|
"review": "./src/review.mjs",
|
|
12
13
|
"configure-claude": "./src/configure-claude.mjs",
|
|
13
14
|
"start-screen": "./src/start-screen.mjs",
|
package/src/claude.lib.mjs
CHANGED
|
@@ -687,6 +687,8 @@ export const executeClaudeCommand = async params => {
|
|
|
687
687
|
// so the comment-posting path can honor them; flags default to false.
|
|
688
688
|
skipOutputSanitization: argv['dangerously-skip-output-sanitization'] === true,
|
|
689
689
|
skipActiveTokensOutputSanitization: argv['dangerously-skip-active-tokens-output-sanitization'] === true,
|
|
690
|
+
// Issue #1843: upload & embed images by default; --no-interactive-image-upload opts out.
|
|
691
|
+
imageUploadEnabled: argv['interactive-image-upload'] !== false,
|
|
690
692
|
});
|
|
691
693
|
} else if (argv.interactiveMode) {
|
|
692
694
|
await log('⚠️ Interactive mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core, offline-testable logic for the `cleanup` command (issue #1848).
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately avoids any top-level network access (no `use-m`
|
|
5
|
+
* fetch) and any side effects so it can be unit-tested without a network
|
|
6
|
+
* connection or a real filesystem. All OS interaction (reading /tmp, querying
|
|
7
|
+
* `$ --status`, scanning /proc, deleting paths, apt cleanup) lives in
|
|
8
|
+
* `cleanup.os.lib.mjs` / `cleanup.mjs`; this module only contains pure
|
|
9
|
+
* classification, parsing and formatting helpers.
|
|
10
|
+
*
|
|
11
|
+
* The classification mirrors the manual workflow described in the issue: list
|
|
12
|
+
* the temporary directories under the tmp root, figure out which ones belong to
|
|
13
|
+
* currently-running solve tasks (by branch name, the same way solve.mjs derives
|
|
14
|
+
* branches), and keep those while removing the rest. Protected system paths such
|
|
15
|
+
* as `/tmp/start-command/` are always preserved unless explicitly forced.
|
|
16
|
+
*
|
|
17
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1848
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { isValidIssueBranchName } from './solve.branch.lib.mjs';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Directory names directly under the tmp root that must never be removed by
|
|
24
|
+
* default because deleting them would interfere with the system's ability to
|
|
25
|
+
* run or be debugged. `start-command` holds the isolation session logs that
|
|
26
|
+
* `$ --status`, /log and /terminal_watch rely on.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_PROTECTED_NAMES = ['start-command'];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* System-owned temp entries that we never touch even in `--all` mode unless the
|
|
32
|
+
* user explicitly opts in. These are created by the OS / desktop / language
|
|
33
|
+
* runtimes and removing them mid-flight can break unrelated processes.
|
|
34
|
+
*/
|
|
35
|
+
export const SYSTEM_PROTECTED_PATTERNS = [/^\.X11-unix$/, /^\.XIM-unix$/, /^\.ICE-unix$/, /^\.font-unix$/, /^\.Test-unix$/, /^systemd-private-/, /^snap-private-tmp$/, /^snap\./, /^\.snap/, /^dbus-/, /^ssh-/, /^hsperfdata_/, /^\.org\.chromium\./, /^\.com\.google\.Chrome\./];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Patterns for temporary entries that are unambiguously created by hive-mind
|
|
39
|
+
* (solve.mjs, github.lib.mjs, claude.lib.mjs, telegram-*, etc.). These are safe
|
|
40
|
+
* to delete when they are not tied to an active task. Each entry has a `name`
|
|
41
|
+
* (for reporting) and a `regex` matched against the basename under the tmp root.
|
|
42
|
+
*
|
|
43
|
+
* Sources are referenced inline so future maintainers can keep this list in
|
|
44
|
+
* sync with the code that produces the files.
|
|
45
|
+
*/
|
|
46
|
+
export const HIVE_MIND_TEMP_PATTERNS = [
|
|
47
|
+
// solve.repository.lib.mjs / solve.execution.lib.mjs workspace clones
|
|
48
|
+
{ name: 'solve workspace clone', regex: /^gh-issue-solver-\d+$/ },
|
|
49
|
+
{ name: 'solve resume workspace', regex: /^gh-issue-solver-resume-.+$/ },
|
|
50
|
+
// solve.repository.lib.mjs buildWorkspacePath parent dir
|
|
51
|
+
{ name: 'solve workspace root', regex: /^hive-mind-solve-gh-/ },
|
|
52
|
+
// github.lib.mjs log download working dirs
|
|
53
|
+
{ name: 'solution draft log dir', regex: /^log-tmp-solution-draft-log-/ },
|
|
54
|
+
// claude.lib.mjs MCP config temp files
|
|
55
|
+
{ name: 'claude MCP config', regex: /^claude-mcp-no-useless-.+\.json$/ },
|
|
56
|
+
{ name: 'claude MCP config', regex: /^claude-mcp-.+\.json$/ },
|
|
57
|
+
// github.lib.mjs comment / body temp files
|
|
58
|
+
{ name: 'solution draft log', regex: /^solution-draft-log-.+\.txt$/ },
|
|
59
|
+
{ name: 'log upload comment', regex: /^log-upload-comment-.+\.md$/ },
|
|
60
|
+
{ name: 'log comment', regex: /^log-comment-.+\.md$/ },
|
|
61
|
+
// github-error-reporter.lib.mjs
|
|
62
|
+
{ name: 'issue body temp', regex: /^hive-mind-issue-body-.+\.md$/ },
|
|
63
|
+
// solve.auto-pr.lib.mjs / solve.results.lib.mjs
|
|
64
|
+
{ name: 'PR body temp', regex: /^pr-body-.+$/ },
|
|
65
|
+
{ name: 'PR title temp', regex: /^pr-title-.+\.txt$/ },
|
|
66
|
+
// solve.progress-monitoring.lib.mjs
|
|
67
|
+
{ name: 'PR progress temp', regex: /^pr-progress-.+$/ },
|
|
68
|
+
// telegram-top-command.lib.mjs
|
|
69
|
+
{ name: 'telegram top output', regex: /^top-output-.+\.txt$/ },
|
|
70
|
+
// start-screen.mjs
|
|
71
|
+
{ name: 'screen ready marker', regex: /^screen-ready-.+\.marker$/ },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a GitHub issue/PR URL out of an arbitrary string (e.g. a solve command
|
|
76
|
+
* line). Self-contained so this module stays offline-safe (github.lib.mjs is
|
|
77
|
+
* not import-safe because of its top-level use-m fetch).
|
|
78
|
+
*
|
|
79
|
+
* @param {string} url
|
|
80
|
+
* @returns {{owner: string, repo: string, type: 'issue'|'pull', number: number}|null}
|
|
81
|
+
*/
|
|
82
|
+
export function parseTaskUrl(url) {
|
|
83
|
+
if (!url || typeof url !== 'string') return null;
|
|
84
|
+
const match = url.match(/github\.com[/:]([^/\s]+)\/([^/\s#]+?)(?:\.git)?\/(issues|issue|pull|pulls)\/(\d+)/i);
|
|
85
|
+
if (!match) return null;
|
|
86
|
+
const type = /^pull/i.test(match[3]) ? 'pull' : 'issue';
|
|
87
|
+
return {
|
|
88
|
+
owner: match[1],
|
|
89
|
+
repo: match[2],
|
|
90
|
+
type,
|
|
91
|
+
number: Number(match[4]),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract all GitHub issue/PR references from a command line string. A solve
|
|
97
|
+
* command typically takes the URL as its first positional argument, but we scan
|
|
98
|
+
* the whole string to be tolerant of flag ordering.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} command
|
|
101
|
+
* @returns {Array<{owner: string, repo: string, type: 'issue'|'pull', number: number}>}
|
|
102
|
+
*/
|
|
103
|
+
export function extractTaskRefsFromCommand(command) {
|
|
104
|
+
if (!command || typeof command !== 'string') return [];
|
|
105
|
+
const refs = [];
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const re = /github\.com[/:]([^/\s]+)\/([^/\s#]+?)(?:\.git)?\/(issues|issue|pull|pulls)\/(\d+)/gi;
|
|
108
|
+
let m;
|
|
109
|
+
while ((m = re.exec(command)) !== null) {
|
|
110
|
+
const ref = parseTaskUrl(m[0]);
|
|
111
|
+
if (!ref) continue;
|
|
112
|
+
const key = `${ref.owner}/${ref.repo}#${ref.number}:${ref.type}`;
|
|
113
|
+
if (seen.has(key)) continue;
|
|
114
|
+
seen.add(key);
|
|
115
|
+
refs.push(ref);
|
|
116
|
+
}
|
|
117
|
+
return refs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Normalise an owner/repo pair extracted from a git remote URL.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} remoteUrl
|
|
124
|
+
* @returns {{owner: string, repo: string}|null}
|
|
125
|
+
*/
|
|
126
|
+
export function parseRemoteUrl(remoteUrl) {
|
|
127
|
+
if (!remoteUrl || typeof remoteUrl !== 'string') return null;
|
|
128
|
+
// git@github.com:owner/repo.git OR https://github.com/owner/repo(.git)
|
|
129
|
+
const sshMatch = remoteUrl.match(/^[^@]+@[^:]+:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
130
|
+
if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
131
|
+
const httpMatch = remoteUrl.match(/^[a-z]+:\/\/[^/]+\/([^/]+)\/(.+?)(?:\.git)?$/i);
|
|
132
|
+
if (httpMatch) return { owner: httpMatch[1], repo: httpMatch[2] };
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sameRepo(a, b) {
|
|
137
|
+
if (!a || !b) return false;
|
|
138
|
+
return a.owner.toLowerCase() === b.owner.toLowerCase() && a.repo.toLowerCase() === b.repo.toLowerCase();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the set of "active task matchers" from the running session task list.
|
|
143
|
+
* For PR tasks the resolved head branch is matched exactly; for issue tasks we
|
|
144
|
+
* fall back to the `issue-{number}-{hex}` prefix (the random hex is unknown from
|
|
145
|
+
* the URL alone) combined with a repo match.
|
|
146
|
+
*
|
|
147
|
+
* @param {Array<{owner, repo, type, number, branch?: string|null}>} activeTasks
|
|
148
|
+
* @returns {Array<{owner, repo, type, issueNumber: number|null, branch: string|null}>}
|
|
149
|
+
*/
|
|
150
|
+
export function buildActiveMatchers(activeTasks) {
|
|
151
|
+
const matchers = [];
|
|
152
|
+
for (const task of activeTasks || []) {
|
|
153
|
+
if (!task) continue;
|
|
154
|
+
matchers.push({
|
|
155
|
+
owner: task.owner,
|
|
156
|
+
repo: task.repo,
|
|
157
|
+
type: task.type,
|
|
158
|
+
issueNumber: task.type === 'issue' ? task.number : (task.issueNumber ?? null),
|
|
159
|
+
branch: task.branch || null,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return matchers;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Decide whether a folder's git info matches one of the active task matchers.
|
|
167
|
+
*
|
|
168
|
+
* @param {{branch: string|null, remotes: Array<{owner, repo}>}|null} gitInfo
|
|
169
|
+
* @param {Array} matchers - from buildActiveMatchers
|
|
170
|
+
* @returns {Object|null} the matched matcher, or null
|
|
171
|
+
*/
|
|
172
|
+
export function folderMatchesActiveTask(gitInfo, matchers) {
|
|
173
|
+
if (!gitInfo || !Array.isArray(matchers)) return null;
|
|
174
|
+
const remotes = gitInfo.remotes || [];
|
|
175
|
+
for (const m of matchers) {
|
|
176
|
+
// 1. Exact branch match (covers PR continue-mode and any known branch).
|
|
177
|
+
if (m.branch && gitInfo.branch && gitInfo.branch === m.branch) {
|
|
178
|
+
return m;
|
|
179
|
+
}
|
|
180
|
+
// 2. issue-{number}-{hex} prefix match scoped to the same repository.
|
|
181
|
+
if (m.issueNumber != null && gitInfo.branch && isValidIssueBranchName(gitInfo.branch, m.issueNumber)) {
|
|
182
|
+
const repoMatches = remotes.length === 0 || remotes.some(r => sameRepo(r, m));
|
|
183
|
+
if (repoMatches) return m;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function matchesAny(name, patterns) {
|
|
190
|
+
return patterns.some(p => (p.regex || p).test(name));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Identify the hive-mind temp pattern a name matches, if any.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} name
|
|
197
|
+
* @returns {{name: string}|null}
|
|
198
|
+
*/
|
|
199
|
+
export function matchHiveMindPattern(name) {
|
|
200
|
+
return HIVE_MIND_TEMP_PATTERNS.find(p => p.regex.test(name)) || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Pure classification of a single temp entry into keep/remove with a reason.
|
|
205
|
+
*
|
|
206
|
+
* Reason precedence (highest first): protected > self > active-process >
|
|
207
|
+
* active-task > dirty-worktree > hive-mind-temp (remove) > all-mode (remove) >
|
|
208
|
+
* unrecognized (keep).
|
|
209
|
+
*
|
|
210
|
+
* @param {{name: string, path: string, isDirectory: boolean}} entry
|
|
211
|
+
* @param {Object} ctx
|
|
212
|
+
* @param {string[]} ctx.protectedNames
|
|
213
|
+
* @param {boolean} ctx.forceStartCommand
|
|
214
|
+
* @param {boolean} ctx.includeSystem - allow classifying system entries in --all
|
|
215
|
+
* @param {boolean} ctx.includeAll - consider non-hive-mind entries for removal
|
|
216
|
+
* @param {boolean} ctx.keepDirty
|
|
217
|
+
* @param {Set<string>} ctx.selfPaths - absolute paths the cleanup process itself uses
|
|
218
|
+
* @param {Set<string>} ctx.heldPaths - absolute paths held by running processes
|
|
219
|
+
* @param {Array} ctx.matchers - active task matchers
|
|
220
|
+
* @param {Map<string,{branch, remotes, dirty}>} ctx.gitInfoByPath
|
|
221
|
+
* @returns {{action: 'keep'|'remove', reason: string}}
|
|
222
|
+
*/
|
|
223
|
+
export function classifyEntry(entry, ctx) {
|
|
224
|
+
const { protectedNames = DEFAULT_PROTECTED_NAMES, forceStartCommand = false, includeSystem = false, includeAll = false, keepDirty = true, selfPaths = new Set(), heldPaths = new Set(), matchers = [], gitInfoByPath = new Map() } = ctx || {};
|
|
225
|
+
|
|
226
|
+
const name = entry.name;
|
|
227
|
+
|
|
228
|
+
// 1. Protected names (start-command can be forced).
|
|
229
|
+
const isStartCommand = name === 'start-command';
|
|
230
|
+
if (protectedNames.includes(name)) {
|
|
231
|
+
if (isStartCommand && forceStartCommand) {
|
|
232
|
+
// fall through to deletion logic below
|
|
233
|
+
} else {
|
|
234
|
+
return { action: 'keep', reason: 'protected' };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 1b. System-owned entries are protected unless explicitly included.
|
|
239
|
+
if (!includeSystem && matchesAny(name, SYSTEM_PROTECTED_PATTERNS)) {
|
|
240
|
+
return { action: 'keep', reason: 'system-protected' };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 2. Paths the cleanup process itself depends on (its own clone / cwd).
|
|
244
|
+
if (selfPaths.has(entry.path)) {
|
|
245
|
+
return { action: 'keep', reason: 'self' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 3. Held open / used as cwd by a running process.
|
|
249
|
+
if (heldPaths.has(entry.path)) {
|
|
250
|
+
return { action: 'keep', reason: 'active-process' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 4. Matches an active solve task by branch / repo.
|
|
254
|
+
const gitInfo = gitInfoByPath.get(entry.path);
|
|
255
|
+
const matched = folderMatchesActiveTask(gitInfo, matchers);
|
|
256
|
+
if (matched) {
|
|
257
|
+
return { action: 'keep', reason: 'active-task' };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 5. Dirty / unpushed worktree: keep by default to avoid losing work.
|
|
261
|
+
if (keepDirty && gitInfo && gitInfo.dirty) {
|
|
262
|
+
return { action: 'keep', reason: 'dirty-worktree' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 6. Recognised hive-mind temp artifact -> safe to remove.
|
|
266
|
+
if (matchHiveMindPattern(name)) {
|
|
267
|
+
return { action: 'remove', reason: isStartCommand ? 'forced-start-command' : 'hive-mind-temp' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// start-command forced but not a hive-mind pattern: still remove when forced.
|
|
271
|
+
if (isStartCommand && forceStartCommand) {
|
|
272
|
+
return { action: 'remove', reason: 'forced-start-command' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 7. --all mode removes anything not otherwise kept.
|
|
276
|
+
if (includeAll) {
|
|
277
|
+
return { action: 'remove', reason: 'all-mode' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 8. Default: leave unrecognised entries alone.
|
|
281
|
+
return { action: 'keep', reason: 'unrecognized' };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Classify a list of temp entries.
|
|
286
|
+
*
|
|
287
|
+
* @param {Array<{name, path, isDirectory, size?: number}>} entries
|
|
288
|
+
* @param {Object} ctx - see classifyEntry
|
|
289
|
+
* @returns {{keep: Array, remove: Array}} each item: {name, path, size, reason}
|
|
290
|
+
*/
|
|
291
|
+
export function classifyEntries(entries, ctx) {
|
|
292
|
+
const keep = [];
|
|
293
|
+
const remove = [];
|
|
294
|
+
for (const entry of entries || []) {
|
|
295
|
+
const { action, reason } = classifyEntry(entry, ctx);
|
|
296
|
+
const record = { name: entry.name, path: entry.path, size: entry.size ?? null, reason };
|
|
297
|
+
if (action === 'remove') remove.push(record);
|
|
298
|
+
else keep.push(record);
|
|
299
|
+
}
|
|
300
|
+
return { keep, remove };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Human-readable, base-1024 byte formatting (matches `du -h` style closely
|
|
305
|
+
* enough for reporting).
|
|
306
|
+
*
|
|
307
|
+
* @param {number|null|undefined} bytes
|
|
308
|
+
* @returns {string}
|
|
309
|
+
*/
|
|
310
|
+
export function formatBytes(bytes) {
|
|
311
|
+
if (bytes == null || Number.isNaN(bytes)) return '?';
|
|
312
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
313
|
+
const units = ['K', 'M', 'G', 'T', 'P'];
|
|
314
|
+
let value = bytes / 1024;
|
|
315
|
+
let unit = 0;
|
|
316
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
317
|
+
value /= 1024;
|
|
318
|
+
unit++;
|
|
319
|
+
}
|
|
320
|
+
const rounded = value >= 10 || Number.isInteger(value) ? Math.round(value) : Math.round(value * 10) / 10;
|
|
321
|
+
return `${rounded}${units[unit]}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Aggregate totals for a classification result.
|
|
326
|
+
*
|
|
327
|
+
* @param {{keep: Array, remove: Array}} classified
|
|
328
|
+
* @returns {{keepCount, removeCount, keepBytes, removeBytes}}
|
|
329
|
+
*/
|
|
330
|
+
export function summarize(classified) {
|
|
331
|
+
const sum = list => list.reduce((acc, item) => acc + (item.size || 0), 0);
|
|
332
|
+
return {
|
|
333
|
+
keepCount: classified.keep.length,
|
|
334
|
+
removeCount: classified.remove.length,
|
|
335
|
+
keepBytes: sum(classified.keep),
|
|
336
|
+
removeBytes: sum(classified.remove),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Human-readable label for a keep/remove reason code.
|
|
342
|
+
* @param {string} reason
|
|
343
|
+
* @returns {string}
|
|
344
|
+
*/
|
|
345
|
+
export function describeReason(reason) {
|
|
346
|
+
const map = {
|
|
347
|
+
protected: 'protected path',
|
|
348
|
+
'system-protected': 'system-owned temp',
|
|
349
|
+
self: 'used by this cleanup process',
|
|
350
|
+
'active-process': 'in use by a running process',
|
|
351
|
+
'active-task': 'belongs to an active task',
|
|
352
|
+
'dirty-worktree': 'has uncommitted/unpushed changes',
|
|
353
|
+
'hive-mind-temp': 'hive-mind temporary artifact',
|
|
354
|
+
'forced-start-command': 'start-command (forced)',
|
|
355
|
+
'all-mode': 'removed by --all',
|
|
356
|
+
unrecognized: 'not a recognised hive-mind artifact',
|
|
357
|
+
};
|
|
358
|
+
return map[reason] || reason;
|
|
359
|
+
}
|