hypomnema 1.0.0

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 (79) hide show
  1. package/.claude-plugin/plugin.json +11 -0
  2. package/LICENSE +21 -0
  3. package/README.ko.md +160 -0
  4. package/README.md +160 -0
  5. package/commands/.gitkeep +0 -0
  6. package/commands/crystallize.md +116 -0
  7. package/commands/doctor.md +66 -0
  8. package/commands/feedback.md +67 -0
  9. package/commands/graph.md +54 -0
  10. package/commands/ingest.md +85 -0
  11. package/commands/init.md +101 -0
  12. package/commands/lint.md +55 -0
  13. package/commands/query.md +55 -0
  14. package/commands/resume.md +48 -0
  15. package/commands/stats.md +39 -0
  16. package/commands/uninstall.md +52 -0
  17. package/commands/upgrade.md +63 -0
  18. package/commands/verify.md +60 -0
  19. package/docs/.gitkeep +0 -0
  20. package/docs/ARCHITECTURE.md +183 -0
  21. package/docs/CONTRIBUTING.md +115 -0
  22. package/docs/TEST-CASES.md +580 -0
  23. package/hooks/.gitkeep +0 -0
  24. package/hooks/hooks.json +109 -0
  25. package/hooks/hypo-auto-commit.mjs +36 -0
  26. package/hooks/hypo-auto-stage.mjs +30 -0
  27. package/hooks/hypo-compact-guard.mjs +71 -0
  28. package/hooks/hypo-cwd-change.mjs +91 -0
  29. package/hooks/hypo-file-watch.mjs +47 -0
  30. package/hooks/hypo-first-prompt.mjs +59 -0
  31. package/hooks/hypo-hot-rebuild.mjs +95 -0
  32. package/hooks/hypo-lookup.mjs +178 -0
  33. package/hooks/hypo-personal-check.mjs +195 -0
  34. package/hooks/hypo-session-start.mjs +141 -0
  35. package/hooks/hypo-shared.mjs +213 -0
  36. package/package.json +37 -0
  37. package/scripts/.gitkeep +0 -0
  38. package/scripts/bump-version.mjs +53 -0
  39. package/scripts/crystallize.mjs +153 -0
  40. package/scripts/doctor.mjs +361 -0
  41. package/scripts/feedback.mjs +130 -0
  42. package/scripts/graph.mjs +183 -0
  43. package/scripts/ingest.mjs +130 -0
  44. package/scripts/init.mjs +515 -0
  45. package/scripts/lib/frontmatter.mjs +11 -0
  46. package/scripts/lib/hypo-ignore.mjs +54 -0
  47. package/scripts/lib/hypo-root.mjs +53 -0
  48. package/scripts/lint.mjs +210 -0
  49. package/scripts/query.mjs +124 -0
  50. package/scripts/resume.mjs +115 -0
  51. package/scripts/stats.mjs +132 -0
  52. package/scripts/uninstall.mjs +188 -0
  53. package/scripts/upgrade.mjs +538 -0
  54. package/scripts/verify.mjs +172 -0
  55. package/skills/.gitkeep +0 -0
  56. package/skills/crystallize/SKILL.md +85 -0
  57. package/skills/graph/SKILL.md +54 -0
  58. package/skills/ingest/SKILL.md +83 -0
  59. package/skills/lint/SKILL.md +55 -0
  60. package/skills/query/SKILL.md +58 -0
  61. package/skills/verify/SKILL.md +92 -0
  62. package/templates/.gitkeep +0 -0
  63. package/templates/.hypoignore +18 -0
  64. package/templates/Home.md +34 -0
  65. package/templates/Overview.md +50 -0
  66. package/templates/SCHEMA.md +106 -0
  67. package/templates/hot.md +22 -0
  68. package/templates/hypo-automation.md +69 -0
  69. package/templates/hypo-config.md +41 -0
  70. package/templates/hypo-guide.md +146 -0
  71. package/templates/hypo-help.md +53 -0
  72. package/templates/index.md +44 -0
  73. package/templates/log.md +25 -0
  74. package/templates/pages/_index.md +61 -0
  75. package/templates/projects/_template/hot.md +28 -0
  76. package/templates/projects/_template/index.md +39 -0
  77. package/templates/projects/_template/prd.md +29 -0
  78. package/templates/projects/_template/session-state.md +9 -0
  79. package/templates/session-state.md +12 -0
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hypomnema uninstall script
4
+ *
5
+ * Removes hook files installed by Hypomnema and strips wiki entries from
6
+ * settings.json, leaving all other user hooks untouched.
7
+ *
8
+ * Usage:
9
+ * node scripts/uninstall.mjs [options]
10
+ *
11
+ * Options:
12
+ * --apply Actually remove files / edit settings.json (default: dry-run)
13
+ * --codex Also remove Codex hooks (~/.codex/hooks/)
14
+ * --hooks-dir=<path> Override Claude hooks directory (default: ~/.claude/hooks)
15
+ */
16
+
17
+ import { existsSync, readFileSync, writeFileSync, rmSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { homedir } from 'os';
20
+ import { fileURLToPath } from 'url';
21
+
22
+ const HOME = homedir();
23
+ const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
24
+ const PKG_ROOT = join(SCRIPT_DIR, '..');
25
+
26
+ // ── arg parsing ──────────────────────────────────────────────────────────────
27
+
28
+ function parseArgs(argv) {
29
+ const args = { apply: false, codex: false, hooksDir: null };
30
+ for (const arg of argv.slice(2)) {
31
+ if (arg === '--apply') args.apply = true;
32
+ else if (arg === '--codex') args.codex = true;
33
+ else if (arg.startsWith('--hooks-dir=')) args.hooksDir = arg.slice(12);
34
+ }
35
+ return args;
36
+ }
37
+
38
+ // ── hook map (single source of truth) ───────────────────────────────────────
39
+
40
+ function loadHookFiles() {
41
+ let cfg;
42
+ try {
43
+ cfg = JSON.parse(readFileSync(join(PKG_ROOT, 'hooks', 'hooks.json'), 'utf-8'));
44
+ } catch {
45
+ console.error('Error: cannot read hooks/hooks.json');
46
+ process.exit(1);
47
+ }
48
+ if (!cfg?.hooks || typeof cfg.hooks !== 'object' || Array.isArray(cfg.hooks)) {
49
+ console.error('Error: hooks/hooks.json must contain a "hooks" object');
50
+ process.exit(1);
51
+ }
52
+
53
+ const hookFiles = new Set();
54
+ const normalizedHookMap = {};
55
+
56
+ for (const [event, groups] of Object.entries(cfg.hooks)) {
57
+ const filenames = [];
58
+ for (const entry of groups) {
59
+ if (typeof entry === 'string') {
60
+ // legacy flat format: entry is a filename
61
+ hookFiles.add(entry);
62
+ filenames.push(entry);
63
+ } else if (entry && Array.isArray(entry.hooks)) {
64
+ // current group format: extract filename from command string
65
+ for (const h of entry.hooks) {
66
+ if (h.type === 'command' && typeof h.command === 'string') {
67
+ const m = h.command.match(/\/hooks\/([^/\s]+\.mjs)$/);
68
+ if (m) { hookFiles.add(m[1]); filenames.push(m[1]); }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ normalizedHookMap[event] = filenames;
74
+ }
75
+
76
+ if (Array.isArray(cfg.shared)) {
77
+ for (const f of cfg.shared) hookFiles.add(f);
78
+ }
79
+ return { hookMap: normalizedHookMap, hookFiles };
80
+ }
81
+
82
+ // ── hook file removal ────────────────────────────────────────────────────────
83
+
84
+ function removeHookFiles(hooksDir, hookFiles, apply) {
85
+ const removed = [], missing = [];
86
+ for (const file of hookFiles) {
87
+ const p = join(hooksDir, file);
88
+ if (existsSync(p)) {
89
+ if (apply) rmSync(p);
90
+ removed.push(p);
91
+ } else {
92
+ missing.push(p);
93
+ }
94
+ }
95
+ return { removed, missing };
96
+ }
97
+
98
+ // ── settings.json cleanup ────────────────────────────────────────────────────
99
+
100
+ function stripSettingsJson(settingsPath, hooksDir, hookMap, apply) {
101
+ if (!existsSync(settingsPath)) return { stripped: [], kept: 0 };
102
+
103
+ let settings;
104
+ try {
105
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
106
+ } catch {
107
+ return { stripped: [], kept: 0, error: `${settingsPath} is not valid JSON — skipping` };
108
+ }
109
+
110
+ if (!settings.hooks || typeof settings.hooks !== 'object') return { stripped: [], kept: 0 };
111
+
112
+ const stripped = [];
113
+ let changed = false;
114
+
115
+ for (const [event, groups] of Object.entries(settings.hooks)) {
116
+ if (!Array.isArray(groups)) continue;
117
+
118
+ const managed = (hookMap[event] ?? []);
119
+ const isHypoHook = h =>
120
+ h.type === 'command' &&
121
+ typeof h.command === 'string' &&
122
+ managed.some(file => h.command === `node ${hooksDir.replace(HOME, '$HOME')}/${file}`);
123
+
124
+ const filtered = groups.flatMap(group => {
125
+ if (!Array.isArray(group.hooks)) return [group];
126
+
127
+ const hypoHooks = group.hooks.filter(h => isHypoHook(h));
128
+ const userHooks = group.hooks.filter(h => !isHypoHook(h));
129
+
130
+ for (const h of hypoHooks) stripped.push(`${event}: ${h.command}`);
131
+
132
+ if (hypoHooks.length === 0) return [group]; // no Hypomnema hooks → keep as-is
133
+ changed = true;
134
+ if (userHooks.length === 0) return []; // all Hypomnema → remove group
135
+ return [{ ...group, hooks: userHooks }]; // mixed → keep only user hooks
136
+ });
137
+
138
+ settings.hooks[event] = filtered;
139
+ }
140
+
141
+ if (changed && apply) {
142
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
143
+ }
144
+
145
+ return { stripped, kept: 0 };
146
+ }
147
+
148
+ // ── main ─────────────────────────────────────────────────────────────────────
149
+
150
+ const args = parseArgs(process.argv);
151
+ const dryRun = !args.apply;
152
+
153
+ const { hookMap, hookFiles } = loadHookFiles();
154
+
155
+ const claudeHooksDir = args.hooksDir ?? join(HOME, '.claude', 'hooks');
156
+ const claudeSettings = join(HOME, '.claude', 'settings.json');
157
+
158
+ const hookResult = removeHookFiles(claudeHooksDir, hookFiles, args.apply);
159
+ const settingsResult = stripSettingsJson(claudeSettings, claudeHooksDir, hookMap, args.apply);
160
+
161
+ let codexHookResult = { removed: [], missing: [] };
162
+ let codexSettingsResult = { stripped: [] };
163
+
164
+ if (args.codex) {
165
+ const codexHooksDir = join(HOME, '.codex', 'hooks');
166
+ const codexSettings = join(HOME, '.codex', 'settings.json');
167
+ codexHookResult = removeHookFiles(codexHooksDir, hookFiles, args.apply);
168
+ codexSettingsResult = stripSettingsJson(codexSettings, codexHooksDir, hookMap, args.apply);
169
+ }
170
+
171
+ // ── report ───────────────────────────────────────────────────────────────────
172
+
173
+ const lines = [];
174
+ if (dryRun) lines.push('[DRY RUN — pass --apply to make changes]');
175
+
176
+ const allRemoved = [...hookResult.removed, ...codexHookResult.removed];
177
+ const allStripped = [...settingsResult.stripped, ...codexSettingsResult.stripped];
178
+
179
+ if (allRemoved.length) lines.push(`✓ Hook files ${dryRun ? 'to remove' : 'removed'} (${allRemoved.length}):\n${allRemoved.map(p => ` ${p}`).join('\n')}`);
180
+ if (allStripped.length) lines.push(`✓ settings.json entries ${dryRun ? 'to remove' : 'removed'} (${allStripped.length}):\n${allStripped.map(p => ` ${p}`).join('\n')}`);
181
+ if (hookResult.missing.length) lines.push(`⊘ Already absent (${hookResult.missing.length}):\n${hookResult.missing.map(p => ` ${p}`).join('\n')}`);
182
+ if (settingsResult.error) lines.push(`⚠ ${settingsResult.error}`);
183
+
184
+ if (!allRemoved.length && !allStripped.length && !hookResult.missing.length) {
185
+ lines.push('Nothing to uninstall — Hypomnema does not appear to be installed.');
186
+ }
187
+
188
+ console.log(lines.join('\n\n'));