synabun 1.3.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 (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Shared utilities for SynaBun Claude Code hooks.
3
+ *
4
+ * Extracted to avoid duplication across session-start, prompt-submit,
5
+ * post-remember, pre-compact, and stop hooks.
6
+ */
7
+
8
+ import { readFileSync, existsSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs';
9
+ import { dirname, join, basename } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ export const DATA_DIR = join(__dirname, '..', '..', 'data');
14
+ export const MCP_DATA_DIR = join(__dirname, '..', '..', 'mcp-server', 'data');
15
+ export const ENV_PATH = join(__dirname, '..', '..', '.env');
16
+ export const HOOK_FEATURES_PATH = join(DATA_DIR, 'hook-features.json');
17
+ export const PENDING_REMEMBER_DIR = join(DATA_DIR, 'pending-remember');
18
+
19
+ // --- Stdin ---
20
+
21
+ export function readStdin() {
22
+ return new Promise((resolve) => {
23
+ if (process.stdin.isTTY) return resolve('{}');
24
+ let data = '';
25
+ process.stdin.setEncoding('utf8');
26
+ process.stdin.on('data', (chunk) => { data += chunk; });
27
+ process.stdin.on('end', () => resolve(data));
28
+ setTimeout(() => resolve(data || '{}'), 2000);
29
+ });
30
+ }
31
+
32
+ // --- Hook features ---
33
+
34
+ export function getHookFeatures() {
35
+ try {
36
+ if (!existsSync(HOOK_FEATURES_PATH)) return {};
37
+ return JSON.parse(readFileSync(HOOK_FEATURES_PATH, 'utf-8'));
38
+ } catch { return {}; }
39
+ }
40
+
41
+ // --- Environment / connection ---
42
+
43
+ function parseEnvFile(filePath) {
44
+ try {
45
+ const content = readFileSync(filePath, 'utf-8');
46
+ const vars = {};
47
+ for (const line of content.split('\n')) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith('#')) continue;
50
+ const eq = trimmed.indexOf('=');
51
+ if (eq === -1) continue;
52
+ vars[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
53
+ }
54
+ return vars;
55
+ } catch { return {}; }
56
+ }
57
+
58
+ export function getActiveConnectionId() {
59
+ const vars = parseEnvFile(ENV_PATH);
60
+ return vars.SYNABUN_ACTIVE_CONNECTION || vars.QDRANT_ACTIVE || 'default';
61
+ }
62
+
63
+ export function getCategoriesPath() {
64
+ const connId = getActiveConnectionId();
65
+ return join(MCP_DATA_DIR, `custom-categories-${connId}.json`);
66
+ }
67
+
68
+ /**
69
+ * Returns the path to the MCP server's categories file (no connection suffix).
70
+ * This is the file the MCP server watches — hooks that need the MCP server to
71
+ * pick up changes (e.g., schema refresh) MUST write to this file.
72
+ */
73
+ export function getMcpCategoriesPath() {
74
+ return join(MCP_DATA_DIR, 'custom-categories.json');
75
+ }
76
+
77
+ // --- Project detection (reads from claude-code-projects.json) ---
78
+
79
+ const PROJECTS_PATH = join(DATA_DIR, 'claude-code-projects.json');
80
+
81
+ export function loadRegisteredProjects() {
82
+ try {
83
+ if (!existsSync(PROJECTS_PATH)) return [];
84
+ return JSON.parse(readFileSync(PROJECTS_PATH, 'utf-8'));
85
+ } catch { return []; }
86
+ }
87
+
88
+ export function normalizeLabel(label) {
89
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
90
+ }
91
+
92
+ export function detectProject(cwd) {
93
+ if (!cwd) return 'global';
94
+ const lower = cwd.toLowerCase().replace(/\\/g, '/');
95
+ const projects = loadRegisteredProjects();
96
+
97
+ // Sort by path length descending — most specific match wins
98
+ const sorted = projects
99
+ .map((p) => ({
100
+ path: p.path.toLowerCase().replace(/\\/g, '/'),
101
+ label: normalizeLabel(p.label),
102
+ }))
103
+ .sort((a, b) => b.path.length - a.path.length);
104
+
105
+ // 1. Exact path prefix match (cwd is inside a registered project)
106
+ for (const p of sorted) {
107
+ if (lower.startsWith(p.path + '/') || lower === p.path) return p.label;
108
+ }
109
+
110
+ // 2. Substring match — cwd folder name contains a registered project's folder name
111
+ // e.g. "SynaBunWebsite" contains "synabun" → matches Synabun project
112
+ for (const p of sorted) {
113
+ const projFolder = basename(p.path).toLowerCase();
114
+ if (lower.includes(projFolder)) return p.label;
115
+ }
116
+
117
+ // 3. Fallback to directory basename
118
+ const base = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
119
+ return base || 'global';
120
+ }
121
+
122
+ // --- Category tree ---
123
+
124
+ export function loadCategories() {
125
+ try {
126
+ const data = JSON.parse(readFileSync(getCategoriesPath(), 'utf-8'));
127
+ if (data.version === 1 && Array.isArray(data.categories)) {
128
+ return data.categories;
129
+ }
130
+ } catch { /* no categories available */ }
131
+ return [];
132
+ }
133
+
134
+ export function buildCategoryTree(categories) {
135
+ if (!categories || categories.length === 0) {
136
+ return '(No categories defined yet. Use category_create to set up your first category.)';
137
+ }
138
+
139
+ const parents = categories.filter((c) => c.is_parent);
140
+ const children = categories.filter((c) => c.parent);
141
+ const standalone = categories.filter((c) => !c.is_parent && !c.parent);
142
+
143
+ const lines = [];
144
+
145
+ for (const parent of parents) {
146
+ const kids = children.filter((c) => c.parent === parent.name);
147
+ lines.push(`${parent.name} (PARENT) — ${parent.description}`);
148
+ for (const kid of kids) {
149
+ lines.push(` └─ ${kid.name} — ${kid.description}`);
150
+ }
151
+ if (kids.length === 0) {
152
+ lines.push(` (no children yet)`);
153
+ }
154
+ }
155
+
156
+ if (standalone.length > 0) {
157
+ for (const cat of standalone) {
158
+ lines.push(`${cat.name} — ${cat.description}`);
159
+ }
160
+ }
161
+
162
+ return lines.join('\n');
163
+ }
164
+
165
+ // --- Auto-category creation ---
166
+
167
+ /**
168
+ * Standalone categories required by hooks. Always created regardless of projects.
169
+ */
170
+ const STANDALONE_DEFAULTS = [
171
+ { name: 'conversations', description: 'Session summaries and conversation indexing' },
172
+ { name: 'communication-style', description: 'User communication patterns and preferences' },
173
+ { name: 'plans', description: 'Implementation plans stored after plan mode approval', is_parent: true, color: '#06d6a0' },
174
+ ];
175
+
176
+ /**
177
+ * Default child categories for every registered project.
178
+ * Created under a project parent, never standalone.
179
+ */
180
+ const PROJECT_CHILDREN = [
181
+ { suffix: 'project', description: 'General project knowledge, decisions, and milestones' },
182
+ { suffix: 'architecture', description: 'System design, tech stack, data flow, and component architecture' },
183
+ { suffix: 'bugs', description: 'Bug fixes, debugging sessions, and known issues' },
184
+ { suffix: 'config', description: 'Configuration, deployment, environment, and infrastructure' },
185
+ ];
186
+
187
+ /**
188
+ * Ensure all registered projects have a parent category with default children,
189
+ * and that standalone hook-required categories exist.
190
+ *
191
+ * Writes to the MCP server's categories file (no connection suffix) so the
192
+ * MCP server's file watcher picks up changes and refreshes tool schemas.
193
+ *
194
+ * Idempotent: only adds missing categories, never modifies or removes existing ones.
195
+ *
196
+ * @returns {{ created: string[], total: number }}
197
+ */
198
+ export function ensureProjectCategories() {
199
+ const projects = loadRegisteredProjects();
200
+ const catPath = getMcpCategoriesPath();
201
+
202
+ let data;
203
+ try {
204
+ data = JSON.parse(readFileSync(catPath, 'utf-8'));
205
+ } catch {
206
+ data = { version: 1, categories: [] };
207
+ }
208
+
209
+ const categories = data.categories || [];
210
+ const existingNames = new Set(categories.map((c) => c.name));
211
+ const created = [];
212
+ const now = new Date().toISOString();
213
+
214
+ // 1. Ensure standalone defaults exist
215
+ for (const def of STANDALONE_DEFAULTS) {
216
+ if (!existingNames.has(def.name)) {
217
+ const cat = { name: def.name, description: def.description, created_at: now };
218
+ if (def.is_parent) cat.is_parent = true;
219
+ if (def.color) cat.color = def.color;
220
+ categories.push(cat);
221
+ existingNames.add(def.name);
222
+ created.push(def.name);
223
+ }
224
+ }
225
+
226
+ // 2. Ensure project parents + children exist
227
+ for (const proj of projects) {
228
+ let label = normalizeLabel(proj.label);
229
+ // Truncate to 16 chars so children stay within 30-char limit (16 + 1 + 12 = 29)
230
+ if (label.length > 16) label = label.slice(0, 16).replace(/-$/, '');
231
+
232
+ // Ensure parent exists
233
+ if (!existingNames.has(label)) {
234
+ categories.push({
235
+ name: label,
236
+ description: `Knowledge and context for the ${proj.label} project`,
237
+ is_parent: true,
238
+ created_at: now,
239
+ });
240
+ existingNames.add(label);
241
+ created.push(label);
242
+ } else {
243
+ // Parent exists — ensure it has is_parent flag
244
+ const existing = categories.find((c) => c.name === label);
245
+ if (existing && !existing.is_parent) existing.is_parent = true;
246
+ }
247
+
248
+ // Ensure each default child exists
249
+ for (const child of PROJECT_CHILDREN) {
250
+ const childName = `${label}-${child.suffix}`;
251
+ if (!existingNames.has(childName)) {
252
+ categories.push({
253
+ name: childName,
254
+ description: `${child.description} for ${proj.label}`,
255
+ parent: label,
256
+ created_at: now,
257
+ });
258
+ existingNames.add(childName);
259
+ created.push(childName);
260
+ }
261
+ }
262
+ }
263
+
264
+ // Only write if we created something new
265
+ if (created.length > 0) {
266
+ data.categories = categories;
267
+ writeFileSync(catPath, JSON.stringify(data, null, 2), 'utf-8');
268
+ }
269
+
270
+ return { created, total: categories.length };
271
+ }
272
+
273
+ /**
274
+ * Build the full category reference block for lazy injection.
275
+ * Includes tree, available names, selection rules, project scoping, and tool notes.
276
+ */
277
+ export function buildCategoryReference(categories, project) {
278
+ const tree = buildCategoryTree(categories);
279
+ const names = categories.map((c) => c.name).join(', ');
280
+
281
+ return [
282
+ `## SynaBun Category Reference`,
283
+ ``,
284
+ `### Project: ${project}`,
285
+ ``,
286
+ tree,
287
+ ``,
288
+ `Available names: ${names || '(none)'}`,
289
+ ``,
290
+ `### Category Selection`,
291
+ ``,
292
+ `1. Match existing child category by description → use it.`,
293
+ `2. No child fits but parent does → \`category_create\` new child under that parent → use it.`,
294
+ `3. No parent fits → \`category_create\` new parent + child → use it.`,
295
+ `4. Uncertain → \`AskUserQuestion\` with 2-3 options + "Create new category" → use what user picks.`,
296
+ ``,
297
+ `NEVER guess. NEVER use parent directly. NEVER skip categorization.`,
298
+ ``,
299
+ `### Project Scoping`,
300
+ ``,
301
+ `- Project-specific (bugs, architecture, config) → "${project}"`,
302
+ `- Universal (language features, library APIs) → "global"`,
303
+ `- Cross-project (shared infra) → "shared"`,
304
+ ].join('\n');
305
+ }
306
+
307
+ // --- Loop staleness cleanup ---
308
+
309
+ const LOOP_STALE_INACTIVITY_MS = 45 * 60 * 1000; // 45 min — browser iterations can be long
310
+
311
+ /**
312
+ * Scan loop directory and delete stale/inactive loop files.
313
+ * No history keeping — ended loops are removed immediately.
314
+ *
315
+ * A loop is stale if:
316
+ * 1. active + exceeded its maxMinutes + 5min grace
317
+ * 2. active + no iteration activity for 10 minutes (session died)
318
+ *
319
+ * @param {string} loopDir - path to data/loop/
320
+ * @returns {{ deactivated: string[], deleted: string[] }}
321
+ */
322
+ export function cleanupStaleLoops(loopDir) {
323
+ const deactivated = [];
324
+ const deleted = [];
325
+ if (!existsSync(loopDir)) return { deactivated, deleted };
326
+
327
+ const now = Date.now();
328
+ try {
329
+ const files = readdirSync(loopDir).filter(f => f.endsWith('.json') && !f.startsWith('pending-'));
330
+ for (const f of files) {
331
+ const fp = join(loopDir, f);
332
+ try {
333
+ const loop = JSON.parse(readFileSync(fp, 'utf-8'));
334
+
335
+ if (loop.active) {
336
+ const lastActivity = new Date(loop.lastIterationAt || loop.startedAt || 0).getTime();
337
+ const noRecentActivity = (now - lastActivity) >= LOOP_STALE_INACTIVITY_MS;
338
+
339
+ if (noRecentActivity) {
340
+ // Stale loop (no activity for 45 min) — delete immediately
341
+ try { unlinkSync(fp); deleted.push(f); } catch { /* ok */ }
342
+ }
343
+ } else {
344
+ // Inactive loop — delete immediately (no history keeping)
345
+ try { unlinkSync(fp); deleted.push(f); } catch { /* ok */ }
346
+ }
347
+ } catch { /* skip corrupt files */ }
348
+ }
349
+ } catch { /* ok */ }
350
+
351
+ // Delete stale pending-*.json files (client never claimed them)
352
+ try {
353
+ const pendingFiles = readdirSync(loopDir)
354
+ .filter(f => f.startsWith('pending-') && f.endsWith('.json'));
355
+ for (const f of pendingFiles) {
356
+ const fp = join(loopDir, f);
357
+ try {
358
+ const loop = JSON.parse(readFileSync(fp, 'utf-8'));
359
+ const lastActivity = new Date(loop.lastIterationAt || loop.startedAt || 0).getTime();
360
+ if ((now - lastActivity) >= LOOP_STALE_INACTIVITY_MS) {
361
+ try { unlinkSync(fp); deleted.push(f); } catch { /* ok */ }
362
+ }
363
+ } catch { /* skip corrupt */ }
364
+ }
365
+ } catch { /* ok */ }
366
+
367
+ return { deactivated, deleted };
368
+ }