@aladac/hu 0.1.0-a1

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.
Files changed (70) hide show
  1. package/.tool-versions +1 -0
  2. package/CLAUDE.md +122 -0
  3. package/HOOKS-DATA-INTEGRATION.md +457 -0
  4. package/SAMPLE.md +378 -0
  5. package/TODO.md +25 -0
  6. package/biome.json +51 -0
  7. package/commands/bootstrap.md +13 -0
  8. package/commands/c.md +1 -0
  9. package/commands/check-name.md +62 -0
  10. package/commands/disk.md +141 -0
  11. package/commands/docs/archive.md +27 -0
  12. package/commands/docs/check-internal.md +53 -0
  13. package/commands/docs/cleanup.md +65 -0
  14. package/commands/docs/consolidate.md +72 -0
  15. package/commands/docs/get.md +101 -0
  16. package/commands/docs/list.md +61 -0
  17. package/commands/docs/sync.md +64 -0
  18. package/commands/docs/update.md +49 -0
  19. package/commands/plans/clear.md +23 -0
  20. package/commands/plans/create.md +71 -0
  21. package/commands/plans/list.md +21 -0
  22. package/commands/plans/sync.md +38 -0
  23. package/commands/reinstall.md +20 -0
  24. package/commands/replicate.md +303 -0
  25. package/commands/warp.md +0 -0
  26. package/doc/README.md +35 -0
  27. package/doc/claude-code/capabilities.md +202 -0
  28. package/doc/claude-code/directory-structure.md +246 -0
  29. package/doc/claude-code/hooks.md +348 -0
  30. package/doc/claude-code/overview.md +109 -0
  31. package/doc/claude-code/plugins.md +273 -0
  32. package/doc/claude-code/sdk-protocols.md +202 -0
  33. package/document-manifest.toml +29 -0
  34. package/justfile +39 -0
  35. package/package.json +33 -0
  36. package/plans/compiled-watching-feather.md +217 -0
  37. package/plans/crispy-crafting-pnueli.md +103 -0
  38. package/plans/greedy-booping-coral.md +146 -0
  39. package/plans/imperative-sleeping-flamingo.md +192 -0
  40. package/plans/jaunty-sprouting-marble.md +171 -0
  41. package/plans/jiggly-discovering-lake.md +68 -0
  42. package/plans/magical-nibbling-spark.md +144 -0
  43. package/plans/mellow-kindling-acorn.md +110 -0
  44. package/plans/recursive-questing-engelbart.md +65 -0
  45. package/plans/serialized-roaming-kernighan.md +227 -0
  46. package/plans/structured-wondering-wirth.md +230 -0
  47. package/plans/vectorized-dreaming-iverson.md +191 -0
  48. package/plans/velvety-enchanting-ocean.md +92 -0
  49. package/plans/wiggly-sparking-pixel.md +48 -0
  50. package/plans/zippy-shimmying-fox.md +188 -0
  51. package/plugins/installed_plugins.json +4 -0
  52. package/sample-hooks.json +298 -0
  53. package/settings.json +24 -0
  54. package/settings.local.json +7 -0
  55. package/src/commands/bump.ts +130 -0
  56. package/src/commands/disk.ts +419 -0
  57. package/src/commands/docs.ts +729 -0
  58. package/src/commands/plans.ts +259 -0
  59. package/src/commands/utils.ts +299 -0
  60. package/src/index.ts +26 -0
  61. package/src/lib/colors.ts +87 -0
  62. package/src/lib/exec.ts +25 -0
  63. package/src/lib/fs.ts +119 -0
  64. package/src/lib/html.ts +205 -0
  65. package/src/lib/spinner.ts +42 -0
  66. package/src/types/index.ts +61 -0
  67. package/tests/lib/colors.test.ts +69 -0
  68. package/tests/lib/fs.test.ts +65 -0
  69. package/tsconfig.json +20 -0
  70. package/vitest.config.ts +15 -0
package/src/lib/fs.ts ADDED
@@ -0,0 +1,119 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ export const HOME = process.env.HOME || process.env.USERPROFILE || '';
5
+ export const CLAUDE_DIR = path.join(HOME, '.claude');
6
+ export const PLANS_DIR = path.join(CLAUDE_DIR, 'plans');
7
+ export const GLOBAL_DOCS = path.join(CLAUDE_DIR, 'documents');
8
+
9
+ export const SYSTEM_DOCS = [
10
+ 'MODELS.md',
11
+ 'PLAN.md',
12
+ 'TODO.md',
13
+ 'TEST.md',
14
+ 'REFACTOR.md',
15
+ 'README.md',
16
+ 'CLAUDE.md',
17
+ 'LICENSE.md',
18
+ 'CHANGELOG.md',
19
+ 'CONTRIBUTING.md',
20
+ ];
21
+
22
+ /**
23
+ * Find the git repository root from current directory
24
+ */
25
+ export function findRepoRoot(): string {
26
+ let dir = process.cwd();
27
+ while (dir !== '/') {
28
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
29
+ dir = path.dirname(dir);
30
+ }
31
+ return process.cwd();
32
+ }
33
+
34
+ /**
35
+ * Recursively find all markdown files in a directory
36
+ */
37
+ export function findMarkdownFiles(dir: string, recursive = true): string[] {
38
+ const files: string[] = [];
39
+ if (!fs.existsSync(dir)) return files;
40
+
41
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
42
+ for (const entry of entries) {
43
+ const fullPath = path.join(dir, entry.name);
44
+ if (entry.isDirectory() && recursive) {
45
+ files.push(...findMarkdownFiles(fullPath, recursive));
46
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
47
+ files.push(fullPath);
48
+ }
49
+ }
50
+ return files;
51
+ }
52
+
53
+ /**
54
+ * Parse YAML frontmatter from markdown content
55
+ */
56
+ export function parseFrontmatter(content: string): Record<string, string> {
57
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
58
+ if (!match) return {};
59
+
60
+ const fm: Record<string, string> = {};
61
+ const lines = match[1].split('\n');
62
+ for (const line of lines) {
63
+ const [key, ...rest] = line.split(':');
64
+ if (key && rest.length) {
65
+ fm[key.trim()] = rest.join(':').trim();
66
+ }
67
+ }
68
+ return fm;
69
+ }
70
+
71
+ /**
72
+ * Count checkboxes in markdown content
73
+ */
74
+ export function countCheckboxes(content: string): { pending: number; completed: number } {
75
+ const pending = (content.match(/- \[ \]/g) || []).length;
76
+ const completed = (content.match(/- \[x\]/gi) || []).length;
77
+ return { pending, completed };
78
+ }
79
+
80
+ /**
81
+ * Safe file read - returns null if file doesn't exist
82
+ */
83
+ export function readFile(filePath: string): string | null {
84
+ try {
85
+ return fs.readFileSync(filePath, 'utf-8');
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Safe file write
93
+ */
94
+ export function writeFile(filePath: string, content: string): boolean {
95
+ try {
96
+ fs.writeFileSync(filePath, content);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Check if path exists
105
+ */
106
+ export function exists(filePath: string): boolean {
107
+ return fs.existsSync(filePath);
108
+ }
109
+
110
+ /**
111
+ * Get file stats or null
112
+ */
113
+ export function getStats(filePath: string): fs.Stats | null {
114
+ try {
115
+ return fs.statSync(filePath);
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
@@ -0,0 +1,205 @@
1
+ import * as cheerio from 'cheerio';
2
+ import TurndownService from 'turndown';
3
+
4
+ // Content selectors to try (in order of preference)
5
+ const CONTENT_SELECTORS = [
6
+ 'article',
7
+ 'main',
8
+ '[role="main"]',
9
+ '.main-content',
10
+ '.content',
11
+ '.post-content',
12
+ '.article-content',
13
+ '.documentation',
14
+ '.docs-content',
15
+ '.markdown-body',
16
+ '#content',
17
+ '#main',
18
+ '.prose',
19
+ ];
20
+
21
+ // Elements to remove for cleaner output
22
+ const REMOVE_SELECTORS = [
23
+ 'script',
24
+ 'style',
25
+ 'noscript',
26
+ 'iframe',
27
+ 'svg',
28
+ 'nav',
29
+ 'header:not(article header)',
30
+ 'footer:not(article footer)',
31
+ '.nav',
32
+ '.navigation',
33
+ '.sidebar',
34
+ '.menu',
35
+ '.toc',
36
+ '.table-of-contents',
37
+ '.breadcrumb',
38
+ '.breadcrumbs',
39
+ '.ads',
40
+ '.advertisement',
41
+ '.ad-container',
42
+ '.social-share',
43
+ '.share-buttons',
44
+ '.comments',
45
+ '.comment-section',
46
+ '[aria-hidden="true"]',
47
+ '.sr-only',
48
+ '.visually-hidden',
49
+ '.cookie-banner',
50
+ '.popup',
51
+ '.modal',
52
+ ];
53
+
54
+ // Extra selectors to remove when clean mode is enabled
55
+ const EXTRA_CLEAN_SELECTORS = [
56
+ 'aside',
57
+ '.related',
58
+ '.related-posts',
59
+ '.author-bio',
60
+ '.newsletter',
61
+ '.subscribe',
62
+ '.cta',
63
+ '.banner',
64
+ '.promo',
65
+ '.feedback',
66
+ '.edit-page',
67
+ '.page-nav',
68
+ '.pagination',
69
+ ];
70
+
71
+ export interface FetchOptions {
72
+ selector?: string;
73
+ includeTitle?: boolean;
74
+ clean?: boolean;
75
+ raw?: boolean;
76
+ }
77
+
78
+ /**
79
+ * Fetch a URL and convert HTML to markdown
80
+ */
81
+ export async function fetchAndConvert(url: string, opts: FetchOptions = {}): Promise<string> {
82
+ const response = await fetch(url, {
83
+ headers: {
84
+ 'User-Agent':
85
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
86
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
87
+ 'Accept-Language': 'en-US,en;q=0.5',
88
+ },
89
+ redirect: 'follow',
90
+ });
91
+
92
+ if (!response.ok) {
93
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
94
+ }
95
+
96
+ const html = await response.text();
97
+ const $ = cheerio.load(html);
98
+
99
+ // Get page title
100
+ const pageTitle = $('title').text().trim() || $('h1').first().text().trim();
101
+
102
+ // Remove unwanted elements
103
+ const selectorsToRemove = opts.clean
104
+ ? [...REMOVE_SELECTORS, ...EXTRA_CLEAN_SELECTORS]
105
+ : REMOVE_SELECTORS;
106
+
107
+ for (const sel of selectorsToRemove) {
108
+ try {
109
+ $(sel).remove();
110
+ } catch {
111
+ // Ignore invalid selectors
112
+ }
113
+ }
114
+
115
+ // Find main content
116
+ // biome-ignore lint/suspicious/noExplicitAny: cheerio types are complex
117
+ let $content: cheerio.Cheerio<any>;
118
+ if (opts.selector) {
119
+ $content = $(opts.selector);
120
+ if ($content.length === 0) {
121
+ console.error(`Warning: Selector "${opts.selector}" not found, using body`);
122
+ $content = $('body');
123
+ }
124
+ } else {
125
+ // Try content selectors in order
126
+ $content = $('body'); // Default
127
+ for (const sel of CONTENT_SELECTORS) {
128
+ const $el = $(sel);
129
+ if ($el.length > 0 && $el.text().trim().length > 100) {
130
+ $content = $el;
131
+ break;
132
+ }
133
+ }
134
+ }
135
+
136
+ // Configure Turndown
137
+ const turndown = new TurndownService({
138
+ headingStyle: 'atx',
139
+ codeBlockStyle: 'fenced',
140
+ bulletListMarker: '-',
141
+ emDelimiter: '*',
142
+ strongDelimiter: '**',
143
+ linkStyle: 'inlined',
144
+ });
145
+
146
+ // Custom rules for better output
147
+ turndown.addRule('preserveCodeBlocks', {
148
+ filter: ['pre'],
149
+ replacement: (content: string, node) => {
150
+ const element = node as unknown as {
151
+ querySelector: (s: string) => { className?: string; textContent?: string } | null;
152
+ textContent?: string;
153
+ };
154
+ const code = element.querySelector('code');
155
+ const lang = code?.className?.match(/language-(\w+)/)?.[1] || '';
156
+ const text = code?.textContent || element.textContent || content;
157
+ return `\n\n\`\`\`${lang}\n${text.trim()}\n\`\`\`\n\n`;
158
+ },
159
+ });
160
+
161
+ turndown.addRule('removeEmptyLinks', {
162
+ filter: (node): boolean => {
163
+ return node.nodeName === 'A' && !node.textContent?.trim();
164
+ },
165
+ replacement: (): string => '',
166
+ });
167
+
168
+ // Convert to markdown
169
+ let markdown = turndown.turndown($content.html() || '');
170
+
171
+ // Post-processing (unless raw mode)
172
+ if (!opts.raw) {
173
+ markdown = postProcess(markdown);
174
+ }
175
+
176
+ // Add title if requested
177
+ if (opts.includeTitle && pageTitle) {
178
+ markdown = `# ${pageTitle}\n\n${markdown}`;
179
+ }
180
+
181
+ return markdown;
182
+ }
183
+
184
+ /**
185
+ * Post-process markdown for cleaner output
186
+ */
187
+ function postProcess(md: string): string {
188
+ const cleaned = md
189
+ // Remove excessive blank lines
190
+ .replace(/\n{4,}/g, '\n\n\n')
191
+ // Remove trailing whitespace
192
+ .replace(/[ \t]+$/gm, '')
193
+ // Remove empty list items
194
+ .replace(/^[-*]\s*$/gm, '')
195
+ // Clean up code blocks
196
+ .replace(/```\n\n+/g, '```\n')
197
+ .replace(/\n\n+```/g, '\n```')
198
+ // Remove lone bullets
199
+ .replace(/^\s*[-*]\s*\n/gm, '')
200
+ // Normalize headers (ensure space after #)
201
+ .replace(/^(#{1,6})([^#\s])/gm, '$1 $2')
202
+ // Trim
203
+ .trim();
204
+ return `${cleaned}\n`;
205
+ }
@@ -0,0 +1,42 @@
1
+ import { c } from './colors.ts';
2
+
3
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
4
+
5
+ let spinnerIndex = 0;
6
+ let spinnerInterval: ReturnType<typeof setInterval> | null = null;
7
+ let currentSpinnerText = '';
8
+
9
+ /**
10
+ * Start an animated spinner with text
11
+ */
12
+ export function startSpinner(text: string): void {
13
+ currentSpinnerText = text;
14
+ spinnerIndex = 0;
15
+ process.stdout.write('\x1b[?25l'); // Hide cursor
16
+ spinnerInterval = setInterval(() => {
17
+ const frame = spinnerFrames[spinnerIndex % spinnerFrames.length];
18
+ process.stdout.write(`\r${c.cyan}${frame}${c.reset} ${currentSpinnerText}`);
19
+ spinnerIndex++;
20
+ }, 80);
21
+ }
22
+
23
+ /**
24
+ * Stop the spinner and show final status
25
+ */
26
+ export function stopSpinner(success = true, finalText: string | null = null): void {
27
+ if (spinnerInterval) {
28
+ clearInterval(spinnerInterval);
29
+ spinnerInterval = null;
30
+ }
31
+ const icon = success ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
32
+ const text = finalText || currentSpinnerText;
33
+ process.stdout.write(`\r${icon} ${text}\x1b[K\n`);
34
+ process.stdout.write('\x1b[?25h'); // Show cursor
35
+ }
36
+
37
+ /**
38
+ * Update spinner text without stopping
39
+ */
40
+ export function updateSpinner(text: string): void {
41
+ currentSpinnerText = text;
42
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Common type definitions for hu
3
+ */
4
+
5
+ export interface DocInfo {
6
+ path: string;
7
+ name: string;
8
+ title: string;
9
+ size: number;
10
+ sizeHuman: string;
11
+ lines: number;
12
+ modified: string;
13
+ source: string | null;
14
+ fetched: string | null;
15
+ }
16
+
17
+ export interface PlanInfo {
18
+ filename: string;
19
+ path: string;
20
+ title: string;
21
+ bullets: string[];
22
+ phases: string[];
23
+ size: number;
24
+ modified: string;
25
+ resumeCmd: string;
26
+ }
27
+
28
+ export interface SystemDocInfo {
29
+ name: string;
30
+ found: boolean;
31
+ path: string | null;
32
+ size: number | null;
33
+ sizeHuman: string | null;
34
+ lines: number | null;
35
+ modified: string | null;
36
+ pending: number | null;
37
+ completed: number | null;
38
+ }
39
+
40
+ export interface CheckboxResult {
41
+ file: string;
42
+ exists: boolean;
43
+ changed: boolean;
44
+ delete?: boolean;
45
+ reason?: string;
46
+ before?: { pending: number; completed: number };
47
+ after?: { pending: number; completed: number };
48
+ removed?: number;
49
+ newContent?: string | null;
50
+ }
51
+
52
+ export interface SpaceHogCheck {
53
+ name: string;
54
+ cmd: string;
55
+ }
56
+
57
+ export interface CleanupSuggestion {
58
+ item: string;
59
+ size: string;
60
+ cmd: string;
61
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatSize, parseSize, progressBar } from '../../src/lib/colors.ts';
3
+
4
+ describe('parseSize', () => {
5
+ it('parses bytes', () => {
6
+ expect(parseSize('100')).toBe(100);
7
+ // Note: 'B' is not recognized as a unit, only K/M/G/T/P
8
+ });
9
+
10
+ it('parses kilobytes', () => {
11
+ expect(parseSize('1K')).toBe(1024);
12
+ expect(parseSize('2.5K')).toBe(2560);
13
+ });
14
+
15
+ it('parses megabytes', () => {
16
+ expect(parseSize('1M')).toBe(1024 * 1024);
17
+ expect(parseSize('10M')).toBe(10 * 1024 * 1024);
18
+ });
19
+
20
+ it('parses gigabytes', () => {
21
+ expect(parseSize('1G')).toBe(1024 ** 3);
22
+ expect(parseSize('50G')).toBe(50 * 1024 ** 3);
23
+ });
24
+
25
+ it('handles invalid input', () => {
26
+ expect(parseSize('')).toBe(0);
27
+ expect(parseSize('abc')).toBe(0);
28
+ });
29
+ });
30
+
31
+ describe('formatSize', () => {
32
+ it('formats bytes', () => {
33
+ expect(formatSize(100)).toBe('100 B');
34
+ expect(formatSize(500)).toBe('500 B');
35
+ });
36
+
37
+ it('formats kilobytes', () => {
38
+ expect(formatSize(1024)).toBe('1.0 KB');
39
+ expect(formatSize(2048)).toBe('2.0 KB');
40
+ });
41
+
42
+ it('formats megabytes', () => {
43
+ expect(formatSize(1024 * 1024)).toBe('1.0 MB');
44
+ expect(formatSize(5 * 1024 * 1024)).toBe('5.0 MB');
45
+ });
46
+
47
+ it('formats gigabytes', () => {
48
+ expect(formatSize(1024 ** 3)).toBe('1.0 GB');
49
+ expect(formatSize(10 * 1024 ** 3)).toBe('10.0 GB');
50
+ });
51
+ });
52
+
53
+ describe('progressBar', () => {
54
+ it('creates green bar for low percentage', () => {
55
+ const bar = progressBar(30, 10);
56
+ expect(bar).toContain('███');
57
+ expect(bar).toContain('\x1b[32m'); // green
58
+ });
59
+
60
+ it('creates yellow bar for medium percentage', () => {
61
+ const bar = progressBar(70, 10);
62
+ expect(bar).toContain('\x1b[33m'); // yellow
63
+ });
64
+
65
+ it('creates red bar for high percentage', () => {
66
+ const bar = progressBar(90, 10);
67
+ expect(bar).toContain('\x1b[31m'); // red
68
+ });
69
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { countCheckboxes, parseFrontmatter } from '../../src/lib/fs.ts';
3
+
4
+ describe('parseFrontmatter', () => {
5
+ it('parses valid frontmatter', () => {
6
+ const content = `---
7
+ source: https://example.com
8
+ fetched: 2024-01-01
9
+ ---
10
+
11
+ # Title
12
+
13
+ Content here`;
14
+
15
+ const result = parseFrontmatter(content);
16
+ expect(result.source).toBe('https://example.com');
17
+ expect(result.fetched).toBe('2024-01-01');
18
+ });
19
+
20
+ it('returns empty object for no frontmatter', () => {
21
+ const content = '# Just a title\n\nNo frontmatter here';
22
+ const result = parseFrontmatter(content);
23
+ expect(result).toEqual({});
24
+ });
25
+
26
+ it('handles frontmatter with colons in values', () => {
27
+ const content = `---
28
+ source: https://example.com:8080/path
29
+ ---
30
+
31
+ Content`;
32
+
33
+ const result = parseFrontmatter(content);
34
+ expect(result.source).toBe('https://example.com:8080/path');
35
+ });
36
+ });
37
+
38
+ describe('countCheckboxes', () => {
39
+ it('counts pending checkboxes', () => {
40
+ const content = `
41
+ - [ ] Task 1
42
+ - [ ] Task 2
43
+ - [x] Done task
44
+ `;
45
+ const result = countCheckboxes(content);
46
+ expect(result.pending).toBe(2);
47
+ expect(result.completed).toBe(1);
48
+ });
49
+
50
+ it('handles case-insensitive completed checkboxes', () => {
51
+ const content = `
52
+ - [x] lowercase
53
+ - [X] uppercase
54
+ `;
55
+ const result = countCheckboxes(content);
56
+ expect(result.completed).toBe(2);
57
+ });
58
+
59
+ it('returns zeros for no checkboxes', () => {
60
+ const content = 'Just plain text';
61
+ const result = countCheckboxes(content);
62
+ expect(result.pending).toBe(0);
63
+ expect(result.completed).toBe(0);
64
+ });
65
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "declaration": false,
12
+ "noEmit": true,
13
+ "allowImportingTsExtensions": true,
14
+ "outDir": "./dist",
15
+ "types": ["node"],
16
+ "lib": ["ES2022"]
17
+ },
18
+ "include": ["src/**/*", "tests/**/*", "vitest.config.ts"],
19
+ "exclude": ["node_modules", "tools", "dist"]
20
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'html'],
11
+ include: ['src/**/*.ts'],
12
+ exclude: ['src/index.ts'],
13
+ },
14
+ },
15
+ });