@hailer/mcp 1.0.29 → 1.1.3

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 (233) hide show
  1. package/.claude/.session-checked +1 -0
  2. package/.claude/agents/agent-ada-skill-builder.md +10 -2
  3. package/.claude/agents/agent-alejandro-function-fields.md +104 -37
  4. package/.claude/agents/agent-bjorn-config-audit.md +41 -21
  5. package/.claude/agents/agent-builder-agent-creator.md +13 -3
  6. package/.claude/agents/agent-code-simplifier.md +53 -0
  7. package/.claude/agents/agent-dmitri-activity-crud.md +126 -11
  8. package/.claude/agents/agent-giuseppe-app-builder.md +212 -22
  9. package/.claude/agents/agent-gunther-mcp-tools.md +7 -36
  10. package/.claude/agents/agent-helga-workflow-config.md +75 -10
  11. package/.claude/agents/agent-igor-activity-mover-automation.md +125 -0
  12. package/.claude/agents/agent-ingrid-doc-templates.md +164 -36
  13. package/.claude/agents/agent-ivan-monolith.md +154 -0
  14. package/.claude/agents/agent-kenji-data-reader.md +15 -8
  15. package/.claude/agents/agent-lars-code-inspector.md +56 -8
  16. package/.claude/agents/agent-marco-mockup-builder.md +110 -0
  17. package/.claude/agents/agent-marcus-api-documenter.md +323 -0
  18. package/.claude/agents/agent-marketplace-publisher.md +232 -72
  19. package/.claude/agents/agent-marketplace-reviewer.md +255 -79
  20. package/.claude/agents/agent-permissions-handler.md +208 -0
  21. package/.claude/agents/agent-simple-writer.md +48 -0
  22. package/.claude/agents/agent-svetlana-code-review.md +127 -14
  23. package/.claude/agents/agent-tanya-test-runner.md +333 -0
  24. package/.claude/agents/agent-ui-designer.md +100 -0
  25. package/.claude/agents/agent-viktor-sql-insights.md +19 -6
  26. package/.claude/agents/agent-web-search.md +55 -0
  27. package/.claude/agents/agent-yevgeni-discussions.md +7 -1
  28. package/.claude/agents/agent-zara-zapier.md +159 -0
  29. package/.claude/commands/app-squad.md +135 -0
  30. package/.claude/commands/audit-squad.md +158 -0
  31. package/.claude/commands/autoplan.md +563 -0
  32. package/.claude/commands/cleanup-squad.md +98 -0
  33. package/.claude/commands/config-squad.md +106 -0
  34. package/.claude/commands/crud-squad.md +87 -0
  35. package/.claude/commands/data-squad.md +97 -0
  36. package/.claude/commands/debug-squad.md +303 -0
  37. package/.claude/commands/doc-squad.md +65 -0
  38. package/.claude/commands/handoff.md +137 -0
  39. package/.claude/commands/health.md +49 -0
  40. package/.claude/commands/help.md +2 -1
  41. package/.claude/commands/help:agents.md +96 -16
  42. package/.claude/commands/help:commands.md +55 -11
  43. package/.claude/commands/help:faq.md +16 -1
  44. package/.claude/commands/help:skills.md +93 -0
  45. package/.claude/commands/hotfix-squad.md +112 -0
  46. package/.claude/commands/integration-squad.md +82 -0
  47. package/.claude/commands/janitor-squad.md +167 -0
  48. package/.claude/commands/learn-auto.md +120 -0
  49. package/.claude/commands/learn.md +120 -0
  50. package/.claude/commands/mcp-list.md +27 -0
  51. package/.claude/commands/onboard-squad.md +140 -0
  52. package/.claude/commands/plan-workspace.md +732 -0
  53. package/.claude/commands/prd.md +131 -0
  54. package/.claude/commands/project-status.md +82 -0
  55. package/.claude/commands/publish.md +138 -0
  56. package/.claude/commands/recap.md +69 -0
  57. package/.claude/commands/restore.md +64 -0
  58. package/.claude/commands/review-squad.md +152 -0
  59. package/.claude/commands/save.md +24 -0
  60. package/.claude/commands/stats.md +19 -0
  61. package/.claude/commands/swarm.md +210 -0
  62. package/.claude/commands/tool-builder.md +3 -1
  63. package/.claude/commands/ws-pull.md +1 -1
  64. package/.claude/commands/yolo-off.md +17 -0
  65. package/.claude/commands/yolo.md +82 -0
  66. package/.claude/hooks/_shared-memory.cjs +305 -0
  67. package/.claude/hooks/_utils.cjs +134 -0
  68. package/.claude/hooks/agent-failure-detector.cjs +164 -79
  69. package/.claude/hooks/agent-usage-logger.cjs +204 -0
  70. package/.claude/hooks/app-edit-guard.cjs +20 -4
  71. package/.claude/hooks/auto-learn.cjs +316 -0
  72. package/.claude/hooks/bash-guard.cjs +282 -0
  73. package/.claude/hooks/builder-mode-manager.cjs +183 -54
  74. package/.claude/hooks/bulk-activity-guard.cjs +283 -0
  75. package/.claude/hooks/context-watchdog.cjs +292 -0
  76. package/.claude/hooks/delegation-reminder.cjs +478 -0
  77. package/.claude/hooks/design-system-lint.cjs +283 -0
  78. package/.claude/hooks/post-scaffold-hook.cjs +16 -3
  79. package/.claude/hooks/prompt-guard.cjs +366 -0
  80. package/.claude/hooks/publish-template-guard.cjs +16 -0
  81. package/.claude/hooks/session-start.cjs +35 -0
  82. package/.claude/hooks/shared-memory-writer.cjs +147 -0
  83. package/.claude/hooks/skill-injector.cjs +140 -0
  84. package/.claude/hooks/skill-usage-logger.cjs +258 -0
  85. package/.claude/hooks/src-edit-guard.cjs +16 -1
  86. package/.claude/hooks/sync-marketplace-agents.cjs +53 -8
  87. package/.claude/scripts/yolo-toggle.cjs +142 -0
  88. package/.claude/settings.json +141 -14
  89. package/.claude/skills/SDK-activity-patterns/SKILL.md +428 -0
  90. package/.claude/skills/SDK-document-templates/SKILL.md +1033 -0
  91. package/.claude/skills/SDK-function-fields/SKILL.md +542 -0
  92. package/.claude/skills/SDK-generate-skill/SKILL.md +92 -0
  93. package/.claude/skills/SDK-init-skill/SKILL.md +127 -0
  94. package/.claude/skills/SDK-insight-queries/SKILL.md +787 -0
  95. package/.claude/skills/SDK-ws-config-skill/SKILL.md +1139 -0
  96. package/.claude/skills/agent-structure/SKILL.md +98 -0
  97. package/.claude/skills/api-documentation-patterns/SKILL.md +474 -0
  98. package/.claude/skills/chrome-mcp-reference/SKILL.md +370 -0
  99. package/.claude/skills/delegation-routing/SKILL.md +202 -0
  100. package/.claude/skills/frontend-design/SKILL.md +254 -0
  101. package/.claude/skills/hailer-activity-mover/SKILL.md +213 -0
  102. package/.claude/skills/hailer-api-client/SKILL.md +518 -0
  103. package/.claude/skills/hailer-app-builder/SKILL.md +939 -11
  104. package/.claude/skills/hailer-apps-pictures/SKILL.md +269 -0
  105. package/.claude/skills/hailer-design-system/SKILL.md +235 -0
  106. package/.claude/skills/hailer-monolith-automations/SKILL.md +686 -0
  107. package/.claude/skills/hailer-permissions-system/SKILL.md +121 -0
  108. package/.claude/skills/hailer-project-protocol/SKILL.md +488 -0
  109. package/.claude/skills/hailer-rest-api/SKILL.md +61 -0
  110. package/.claude/skills/hailer-rest-api/hailer-activities.md +184 -0
  111. package/.claude/skills/hailer-rest-api/hailer-admin.md +473 -0
  112. package/.claude/skills/hailer-rest-api/hailer-calendar.md +256 -0
  113. package/.claude/skills/hailer-rest-api/hailer-feed.md +249 -0
  114. package/.claude/skills/hailer-rest-api/hailer-insights.md +195 -0
  115. package/.claude/skills/hailer-rest-api/hailer-messaging.md +276 -0
  116. package/.claude/skills/hailer-rest-api/hailer-workflows.md +283 -0
  117. package/.claude/skills/insight-join-patterns/SKILL.md +3 -0
  118. package/.claude/skills/integration-patterns/SKILL.md +421 -0
  119. package/.claude/skills/json-only-output/SKILL.md +52 -12
  120. package/.claude/skills/lsp-setup/SKILL.md +160 -0
  121. package/.claude/skills/mcp-direct-tools/SKILL.md +153 -0
  122. package/.claude/skills/optional-parameters/SKILL.md +32 -23
  123. package/.claude/skills/publish-hailer-app/SKILL.md +76 -12
  124. package/.claude/skills/testing-patterns/SKILL.md +630 -0
  125. package/.claude/skills/tool-builder/SKILL.md +250 -0
  126. package/.claude/skills/tool-parameter-usage/SKILL.md +59 -45
  127. package/.claude/skills/tool-response-verification/SKILL.md +82 -48
  128. package/.claude/skills/zapier-hailer-patterns/SKILL.md +581 -0
  129. package/.env.example +26 -7
  130. package/CLAUDE.md +290 -224
  131. package/dist/CLAUDE.md +370 -0
  132. package/dist/app.d.ts +1 -1
  133. package/dist/app.js +101 -101
  134. package/dist/bot/bot-config.d.ts +26 -0
  135. package/dist/bot/bot-config.js +135 -0
  136. package/dist/bot/bot-manager.d.ts +40 -0
  137. package/dist/bot/bot-manager.js +137 -0
  138. package/dist/bot/bot.d.ts +127 -0
  139. package/dist/bot/bot.js +1328 -0
  140. package/dist/bot/operation-logger.d.ts +28 -0
  141. package/dist/bot/operation-logger.js +132 -0
  142. package/dist/bot/services/conversation-manager.d.ts +60 -0
  143. package/dist/bot/services/conversation-manager.js +246 -0
  144. package/dist/bot/services/index.d.ts +9 -0
  145. package/dist/bot/services/index.js +18 -0
  146. package/dist/bot/services/message-classifier.d.ts +42 -0
  147. package/dist/bot/services/message-classifier.js +228 -0
  148. package/dist/bot/services/message-formatter.d.ts +88 -0
  149. package/dist/bot/services/message-formatter.js +411 -0
  150. package/dist/bot/services/session-logger.d.ts +162 -0
  151. package/dist/bot/services/session-logger.js +724 -0
  152. package/dist/bot/services/token-billing.d.ts +78 -0
  153. package/dist/bot/services/token-billing.js +233 -0
  154. package/dist/bot/services/types.d.ts +169 -0
  155. package/dist/bot/services/types.js +12 -0
  156. package/dist/bot/services/typing-indicator.d.ts +23 -0
  157. package/dist/bot/services/typing-indicator.js +60 -0
  158. package/dist/bot/services/workspace-schema-cache.d.ts +122 -0
  159. package/dist/bot/services/workspace-schema-cache.js +506 -0
  160. package/dist/bot/tool-executor.d.ts +28 -0
  161. package/dist/bot/tool-executor.js +48 -0
  162. package/dist/bot/workspace-overview.d.ts +12 -0
  163. package/dist/bot/workspace-overview.js +94 -0
  164. package/dist/cli.d.ts +1 -8
  165. package/dist/cli.js +1 -253
  166. package/dist/config.d.ts +96 -3
  167. package/dist/config.js +148 -37
  168. package/dist/core.d.ts +5 -0
  169. package/dist/core.js +61 -8
  170. package/dist/lib/discussion-lock.d.ts +42 -0
  171. package/dist/lib/discussion-lock.js +110 -0
  172. package/dist/lib/logger.d.ts +0 -1
  173. package/dist/lib/logger.js +39 -23
  174. package/dist/lib/request-logger.d.ts +77 -0
  175. package/dist/lib/request-logger.js +147 -0
  176. package/dist/mcp/UserContextCache.js +16 -13
  177. package/dist/mcp/hailer-clients.js +18 -17
  178. package/dist/mcp/signal-handler.js +43 -13
  179. package/dist/mcp/tool-registry.d.ts +4 -15
  180. package/dist/mcp/tool-registry.js +94 -32
  181. package/dist/mcp/tools/activity.js +28 -69
  182. package/dist/mcp/tools/app-core.js +9 -4
  183. package/dist/mcp/tools/app-marketplace.js +22 -12
  184. package/dist/mcp/tools/app-member.js +5 -2
  185. package/dist/mcp/tools/app-scaffold.js +32 -18
  186. package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
  187. package/dist/mcp/tools/bot-config/constants.js +94 -0
  188. package/dist/mcp/tools/bot-config/core.d.ts +253 -0
  189. package/dist/mcp/tools/bot-config/core.js +2456 -0
  190. package/dist/mcp/tools/bot-config/index.d.ts +10 -0
  191. package/dist/mcp/tools/bot-config/index.js +59 -0
  192. package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
  193. package/dist/mcp/tools/bot-config/tools.js +15 -0
  194. package/dist/mcp/tools/bot-config/types.d.ts +50 -0
  195. package/dist/mcp/tools/bot-config/types.js +6 -0
  196. package/dist/mcp/tools/discussion.js +107 -77
  197. package/dist/mcp/tools/document.d.ts +11 -0
  198. package/dist/mcp/tools/document.js +741 -0
  199. package/dist/mcp/tools/file.js +5 -2
  200. package/dist/mcp/tools/insight.js +36 -12
  201. package/dist/mcp/tools/investigate.d.ts +9 -0
  202. package/dist/mcp/tools/investigate.js +254 -0
  203. package/dist/mcp/tools/user.d.ts +2 -4
  204. package/dist/mcp/tools/user.js +9 -50
  205. package/dist/mcp/tools/workflow.d.ts +1 -0
  206. package/dist/mcp/tools/workflow.js +164 -52
  207. package/dist/mcp/utils/hailer-api-client.js +26 -17
  208. package/dist/mcp/webhook-handler.d.ts +64 -3
  209. package/dist/mcp/webhook-handler.js +227 -9
  210. package/dist/mcp-server.d.ts +4 -0
  211. package/dist/mcp-server.js +237 -25
  212. package/dist/plugins/bug-fixer/index.d.ts +2 -0
  213. package/dist/plugins/bug-fixer/index.js +18 -0
  214. package/dist/plugins/bug-fixer/tools.d.ts +45 -0
  215. package/dist/plugins/bug-fixer/tools.js +1096 -0
  216. package/package.json +10 -10
  217. package/scripts/test-hal-tools.ts +154 -0
  218. package/.claude/agents/agent-nora-name-functions.md +0 -123
  219. package/.claude/assistant-knowledge.md +0 -23
  220. package/.claude/commands/install-plugin.md +0 -261
  221. package/.claude/commands/list-plugins.md +0 -42
  222. package/.claude/commands/marketplace-setup.md +0 -33
  223. package/.claude/commands/publish-plugin.md +0 -55
  224. package/.claude/commands/uninstall-plugin.md +0 -87
  225. package/.claude/hooks/interactive-mode.cjs +0 -87
  226. package/.claude/hooks/mcp-server-guard.cjs +0 -108
  227. package/.claude/skills/marketplace-publishing.md +0 -155
  228. package/dist/bot/chat-bot.d.ts +0 -31
  229. package/dist/bot/chat-bot.js +0 -357
  230. package/dist/mcp/tools/metrics.d.ts +0 -13
  231. package/dist/mcp/tools/metrics.js +0 -546
  232. package/dist/stdio-server.d.ts +0 -14
  233. package/dist/stdio-server.js +0 -114
@@ -42,10 +42,26 @@
42
42
 
43
43
  const fs = require('fs');
44
44
  const path = require('path');
45
+ const os = require('os');
46
+
47
+ // Skip in yolo mode
48
+ try {
49
+ const statePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'yolo-state.json');
50
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
51
+ if (state.mode === 'yolo') process.exit(0);
52
+ } catch {}
53
+
54
+ // Skip in subagent context - subagents can't use AskUserQuestion or Bash to recover
55
+ // Orchestrator should use --agent-on before spawning, but this is a safety fallback
56
+ if (process.env.CLAUDE_AGENT_ID || process.env.CLAUDE_SUBAGENT) {
57
+ console.log(JSON.stringify({ decision: 'allow' }));
58
+ process.exit(0);
59
+ }
45
60
 
46
- const RELEASE_TRACKER = '/tmp/.claude-released-apps.json';
47
- const BUILDER_MODE_DIR = '/tmp/.claude-builder-mode';
48
- const BUILDER_AGENT_ACTIVE = '/tmp/.claude-builder-agent-active';
61
+ const TEMP_DIR = os.tmpdir();
62
+ const RELEASE_TRACKER = path.join(TEMP_DIR, '.claude-released-apps.json');
63
+ const BUILDER_MODE_DIR = path.join(TEMP_DIR, '.claude-builder-mode');
64
+ const BUILDER_AGENT_ACTIVE = path.join(TEMP_DIR, '.claude-builder-agent-active');
49
65
 
50
66
  // Read hook input from stdin
51
67
  let input = '';
@@ -288,7 +304,7 @@ OR: Release for manual editing (if user explicitly requests)
288
304
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
289
305
 
290
306
  Ask the user first! If they confirm manual editing, run:
291
- Bash: node .claude/hooks/app-edit-guard.cjs --release "${appInfo.path}"
307
+ Bash: node "${process.argv[1].replace(/'/g, "'\\''")}" --release "${appInfo.path}"
292
308
 
293
309
  Then retry your edit.
294
310
  `);
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * <hook-name>auto-learn</hook-name>
4
+ *
5
+ * <purpose>
6
+ * Detects potential learnings from agent responses and prompts user to confirm.
7
+ * Only appends to LEARNINGS.md after user approval.
8
+ * Complements manual /learn command.
9
+ * </purpose>
10
+ *
11
+ * <triggers>
12
+ * - PostToolUse on Task (agent completions)
13
+ * </triggers>
14
+ *
15
+ * <detection-patterns>
16
+ * - "learned that", "discovered that", "found that"
17
+ * - "note:", "important:", "gotcha:", "tip:"
18
+ * - "always", "never", "must", "should" (in context of rules)
19
+ * - "the trick is", "the key is", "turns out"
20
+ * </detection-patterns>
21
+ *
22
+ * <behavior>
23
+ * 1. Scan tool response for learning patterns
24
+ * 2. If found, output prompt for Claude to use AskUserQuestion
25
+ * 3. User confirms which learnings to save
26
+ * 4. Claude runs --save command to append to LEARNINGS.md
27
+ * </behavior>
28
+ */
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const os = require('os');
33
+
34
+ // Skip in yolo mode
35
+ try {
36
+ const statePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'yolo-state.json');
37
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
38
+ if (state.mode === 'yolo') process.exit(0);
39
+ } catch (e) {
40
+ // ENOENT is expected when not in yolo mode - only warn on unexpected errors
41
+ if (e.code !== 'ENOENT' && !e.message.includes('Unexpected')) {
42
+ console.error(`[auto-learn] Warning: ${e.message}`);
43
+ }
44
+ }
45
+
46
+ const LEARNINGS_FILE = path.join(
47
+ process.env.HAILER_INBOX_DIR || path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), 'inbox'),
48
+ 'learnings.md'
49
+ );
50
+ const PENDING_FILE = path.join(os.tmpdir(), '.claude-pending-learnings.json');
51
+
52
+ // Patterns that indicate a learning/insight
53
+ const LEARNING_PATTERNS = [
54
+ /(?:i |we |claude )(?:learned|discovered|found|realized) that (.{20,150})/i,
55
+ /(?:note|important|gotcha|tip|caveat|warning):\s*(.{20,150})/i,
56
+ /(?:the (?:trick|key|solution|fix) (?:is|was)) (.{20,150})/i,
57
+ /(?:turns out|it turns out) (.{20,150})/i,
58
+ /(?:remember to|don't forget to|make sure to) (.{20,100})/i,
59
+ ];
60
+
61
+ // Category detection based on content
62
+ const CATEGORY_PATTERNS = [
63
+ { pattern: /agent|kenji|dmitri|giuseppe|helga|alejandro|viktor/i, category: 'agent' },
64
+ { pattern: /skill|pattern|template/i, category: 'skill' },
65
+ { pattern: /workflow|field|phase|activity/i, category: 'workflow' },
66
+ { pattern: /bug|error|fix|broken/i, category: 'bug' },
67
+ { pattern: /hook|guard|check/i, category: 'hook' },
68
+ ];
69
+
70
+ // Read hook input from stdin
71
+ let input = '';
72
+ process.stdin.setEncoding('utf8');
73
+ process.stdin.on('data', chunk => input += chunk);
74
+ process.stdin.on('end', () => {
75
+ try {
76
+ const data = JSON.parse(input);
77
+ processHook(data);
78
+ } catch (e) {
79
+ console.error(`[auto-learn] Warning: ${e.message}`);
80
+ process.exit(0);
81
+ }
82
+ });
83
+
84
+ function detectCategory(text) {
85
+ for (const { pattern, category } of CATEGORY_PATTERNS) {
86
+ if (pattern.test(text)) {
87
+ return category;
88
+ }
89
+ }
90
+ return 'pattern';
91
+ }
92
+
93
+ function extractLearnings(text) {
94
+ const learnings = [];
95
+
96
+ // Limit input size to prevent ReDoS attacks (100KB max)
97
+ const MAX_INPUT_SIZE = 100000;
98
+ if (text.length > MAX_INPUT_SIZE) {
99
+ text = text.substring(0, MAX_INPUT_SIZE);
100
+ }
101
+
102
+ // Normalize whitespace for multi-line matching
103
+ const normalizedText = text.replace(/\s+/g, ' ');
104
+
105
+ // NOTE: Uses global match then per-result extraction. Acceptable for typical agent output sizes (<100KB).
106
+ // For larger inputs, consider exec() loop instead.
107
+ const matchStartTime = Date.now();
108
+ for (const pattern of LEARNING_PATTERNS) {
109
+ if (Date.now() - matchStartTime > 1000) break; // ReDoS timeout protection
110
+ // Use 'gis' flags for case-insensitive, dotall (multi-line) matching
111
+ const matches = normalizedText.match(new RegExp(pattern.source, 'gis'));
112
+ if (matches) {
113
+ for (const match of matches) {
114
+ const extracted = match.match(new RegExp(pattern.source, 'is'));
115
+ if (extracted && extracted[1]) {
116
+ let learning = extracted[1].trim();
117
+ learning = learning.replace(/\s+/g, ' ').replace(/[.!,;]$/, '').trim();
118
+ if (learning.length >= 20 && !learnings.some(l => l.text === learning)) {
119
+ learnings.push({
120
+ text: learning,
121
+ category: detectCategory(learning)
122
+ });
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ return learnings;
130
+ }
131
+
132
+ function processHook(data) {
133
+ const { tool_name, tool_input, tool_response } = data;
134
+
135
+ // Only process Task completions
136
+ if (tool_name !== 'Task') {
137
+ process.exit(0);
138
+ }
139
+
140
+ if (tool_response === undefined) {
141
+ process.exit(0);
142
+ }
143
+
144
+ const agentName = tool_input?.subagent_type;
145
+ const responseText = typeof tool_response === 'string'
146
+ ? tool_response
147
+ : JSON.stringify(tool_response || '');
148
+
149
+ const learnings = extractLearnings(responseText);
150
+
151
+ if (learnings.length > 0) {
152
+ // Save pending learnings for later confirmation
153
+ fs.writeFileSync(PENDING_FILE, JSON.stringify({
154
+ learnings,
155
+ agentName,
156
+ timestamp: new Date().toISOString()
157
+ }));
158
+
159
+ // Build options for AskUserQuestion (max 4)
160
+ const options = learnings.slice(0, 3).map((l, i) => ({
161
+ label: `Save #${i + 1}`,
162
+ description: l.text.substring(0, 50) + (l.text.length > 50 ? '...' : '')
163
+ }));
164
+ options.push({ label: 'Skip all', description: 'Don\'t save any of these' });
165
+
166
+ const output = `
167
+ <user-prompt-submit-hook>
168
+ 📚 POTENTIAL LEARNING${learnings.length > 1 ? 'S' : ''} DETECTED
169
+
170
+ ${learnings.map((l, i) => `${i + 1}. [${l.category}] ${l.text}`).join('\n')}
171
+
172
+ 👉 Ask user: "Save to inbox/learnings.md?" Options: ${options.map(o => o.label).join(' / ')}
173
+
174
+ If yes, run: node .claude/hooks/auto-learn.cjs --save <indices>
175
+ </user-prompt-submit-hook>
176
+ `;
177
+
178
+ console.log(output);
179
+ }
180
+
181
+ process.exit(0);
182
+ }
183
+
184
+ // CLI: Save specific learnings by index
185
+ if (process.argv[2] === '--save') {
186
+ const indices = process.argv.slice(3)
187
+ .map(n => parseInt(n, 10))
188
+ .filter(n => !isNaN(n))
189
+ .map(n => n - 1);
190
+
191
+ if (!fs.existsSync(PENDING_FILE)) {
192
+ console.error('No pending learnings found');
193
+ process.exit(1);
194
+ }
195
+
196
+ const pending = JSON.parse(fs.readFileSync(PENDING_FILE, 'utf8'));
197
+ const toSave = indices
198
+ .filter(i => i >= 0 && i < pending.learnings.length)
199
+ .map(i => pending.learnings[i]);
200
+
201
+ if (toSave.length === 0) {
202
+ console.log('No valid learnings selected');
203
+ process.exit(0);
204
+ }
205
+
206
+ // Ensure LEARNINGS.md exists (recursive: true is idempotent, no TOCTOU race)
207
+ if (!fs.existsSync(LEARNINGS_FILE)) {
208
+ const dir = path.dirname(LEARNINGS_FILE);
209
+ fs.mkdirSync(dir, { recursive: true });
210
+ fs.writeFileSync(LEARNINGS_FILE, `# Inbox
211
+
212
+ Learnings captured from all projects. Review and integrate into marketplace agents/skills.
213
+
214
+ ## Pending
215
+ <!-- /learn and auto-learn add items here -->
216
+
217
+ ## Applied
218
+ <!-- Move items here after integrating into agents/skills -->
219
+ `);
220
+ }
221
+
222
+ let content = fs.readFileSync(LEARNINGS_FILE, 'utf8');
223
+ const timestamp = new Date().toISOString().split('T')[0];
224
+
225
+ const entries = toSave.map(l => {
226
+ const source = pending.agentName ? `from ${pending.agentName}` : '';
227
+ const autoTag = '🤖 auto-learn';
228
+ return `- [${l.category}] ${l.text} _(${autoTag}${source ? `, ${source}` : ''}, ${timestamp})_`;
229
+ }).join('\n');
230
+
231
+ // Add under Pending section
232
+ const pendingMatch = content.match(/^(## Pending\s*\n(?:<!--[^>]*-->\s*\n)?)/m);
233
+ if (pendingMatch) {
234
+ const insertPoint = content.indexOf(pendingMatch[0]) + pendingMatch[0].length;
235
+ content = content.slice(0, insertPoint) + entries + '\n' + content.slice(insertPoint);
236
+ } else {
237
+ // Fallback: append to end if no Pending section found
238
+ content += `\n## Pending\n${entries}\n`;
239
+ }
240
+
241
+ // Atomic write: temp file + rename to prevent corruption
242
+ const tmpFile = LEARNINGS_FILE + '.tmp-' + process.pid;
243
+ fs.writeFileSync(tmpFile, content);
244
+ fs.renameSync(tmpFile, LEARNINGS_FILE);
245
+
246
+ // Safe unlink: ignore ENOENT if file already deleted
247
+ try {
248
+ fs.unlinkSync(PENDING_FILE);
249
+ } catch (err) {
250
+ if (err.code !== 'ENOENT') throw err;
251
+ }
252
+
253
+ console.log(`✅ Saved ${toSave.length} learning(s) to inbox/learnings.md`);
254
+ process.exit(0);
255
+ }
256
+
257
+ // CLI: Clear pending
258
+ if (process.argv[2] === '--clear') {
259
+ if (fs.existsSync(PENDING_FILE)) {
260
+ fs.unlinkSync(PENDING_FILE);
261
+ console.log('✅ Cleared pending learnings');
262
+ } else {
263
+ console.log('No pending learnings');
264
+ }
265
+ process.exit(0);
266
+ }
267
+
268
+ // CLI: Show pending
269
+ if (process.argv[2] === '--pending') {
270
+ if (!fs.existsSync(PENDING_FILE)) {
271
+ console.log('No pending learnings');
272
+ process.exit(0);
273
+ }
274
+ const pending = JSON.parse(fs.readFileSync(PENDING_FILE, 'utf8'));
275
+ console.log('Pending learnings:');
276
+ pending.learnings.forEach((l, i) => {
277
+ console.log(` ${i + 1}. [${l.category}] ${l.text}`);
278
+ });
279
+ process.exit(0);
280
+ }
281
+
282
+ // CLI: Test
283
+ if (process.argv[2] === '--test' && process.argv[3]) {
284
+ const text = process.argv.slice(3).join(' ');
285
+ const learnings = extractLearnings(text);
286
+ if (learnings.length === 0) {
287
+ console.log('No learnings detected');
288
+ } else {
289
+ console.log('Detected:');
290
+ learnings.forEach(l => console.log(` [${l.category}] ${l.text}`));
291
+ }
292
+ process.exit(0);
293
+ }
294
+
295
+ // CLI: Help
296
+ if (process.argv[2] === '--help' || process.argv[2] === '-h') {
297
+ console.log(`
298
+ Auto-Learn Hook - Detects learnings and prompts for confirmation
299
+
300
+ Usage:
301
+ node auto-learn.cjs --save 1 2 3 Save learnings by index
302
+ node auto-learn.cjs --pending Show pending learnings
303
+ node auto-learn.cjs --clear Clear pending learnings
304
+ node auto-learn.cjs --test <text> Test detection on sample text
305
+ node auto-learn.cjs --help Show this help
306
+
307
+ Workflow:
308
+ 1. Hook detects learning in agent response
309
+ 2. Prompts Claude to ask user for confirmation
310
+ 3. User selects which to save
311
+ 4. Claude runs --save with indices
312
+
313
+ Complements manual /learn command.
314
+ `);
315
+ process.exit(0);
316
+ }
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * <hook-name>bash-guard</hook-name>
4
+ *
5
+ * <purpose>
6
+ * Consolidated Bash PreToolUse guard. Replaces 4 separate hooks:
7
+ * - destructive-command-guard (blocks dangerous commands)
8
+ * - mcp-server-guard (blocks server start commands)
9
+ * - workspace-pull-guard (warns before pull with dirty workspace)
10
+ * - workspace-auto-save (auto-commits before push)
11
+ *
12
+ * Single Node process instead of 4 = faster, no timeouts.
13
+ * </purpose>
14
+ *
15
+ * <triggers>PreToolUse on Bash</triggers>
16
+ * @version 1.0.0
17
+ */
18
+
19
+ const { spawnSync } = require('child_process');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const os = require('os');
23
+
24
+ const ALLOW = JSON.stringify({ decision: 'allow' });
25
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
26
+
27
+ // --- CLI modes ---
28
+ if (process.argv[2] === '--bypass' && process.argv[3]) {
29
+ const command = process.argv[3];
30
+ const bypass = { command, createdAt: Date.now(), expiresAt: Date.now() + 120000 };
31
+ const BYPASS_FILE = path.join(os.tmpdir(), '.claude-destructive-bypass.json');
32
+ const tmpFile = BYPASS_FILE + '.tmp-' + process.pid;
33
+ fs.writeFileSync(tmpFile, JSON.stringify(bypass, null, 2));
34
+ fs.renameSync(tmpFile, BYPASS_FILE);
35
+ console.log(`Bypass created for: ${command}`);
36
+ console.log('Expires in 2 minutes. Retry the command now.');
37
+ process.exit(0);
38
+ }
39
+ if (process.argv[2] === '--history') {
40
+ try {
41
+ const result = spawnSync('git', ['log', '--oneline', '--grep=Auto-save workspace', '-20'], {
42
+ cwd: projectDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
43
+ });
44
+ console.log((result.stdout || '').trim() || 'No auto-saves found');
45
+ } catch { console.log('Could not read git log'); }
46
+ process.exit(0);
47
+ }
48
+ if (process.argv[2] === '--help' || process.argv[2] === '-h') {
49
+ console.log(`
50
+ Bash Guard - Consolidated Bash PreToolUse hook
51
+
52
+ Combines: destructive-command-guard, mcp-server-guard, workspace-pull-guard, workspace-auto-save
53
+
54
+ Usage:
55
+ node bash-guard.cjs --bypass '<cmd>' Create one-time bypass for blocked command
56
+ node bash-guard.cjs --history Show recent workspace auto-saves
57
+ node bash-guard.cjs --help Show this help
58
+ `);
59
+ process.exit(0);
60
+ }
61
+
62
+ // --- Yolo mode check ---
63
+ let isYolo = false;
64
+ try {
65
+ const statePath = path.join(projectDir, '.claude', 'yolo-state.json');
66
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
67
+ isYolo = state.mode === 'yolo';
68
+ } catch {}
69
+
70
+ // --- Read stdin ---
71
+ let input = '';
72
+ process.stdin.setEncoding('utf8');
73
+ process.stdin.on('data', chunk => input += chunk);
74
+ process.stdin.on('end', () => {
75
+ try {
76
+ const data = JSON.parse(input || '{}');
77
+ processHook(data);
78
+ } catch {
79
+ allow();
80
+ }
81
+ });
82
+
83
+ function allow(msg) {
84
+ if (msg) console.error('[bash-guard] ' + msg);
85
+ console.log(ALLOW);
86
+ process.exit(0);
87
+ }
88
+
89
+ function block(reason) {
90
+ console.log(JSON.stringify({ decision: 'block', reason }));
91
+ process.exit(0);
92
+ }
93
+
94
+ function processHook(data) {
95
+ const { tool_name, tool_input } = data;
96
+ if (tool_name !== 'Bash') { allow(); return; }
97
+
98
+ const command = tool_input?.command || '';
99
+ if (!command) { allow(); return; }
100
+
101
+ // Run checks in order of specificity
102
+ if (!isYolo) {
103
+ checkDestructive(command);
104
+ checkMcpServer(command);
105
+ checkWorkspacePull(command);
106
+ checkWorkspaceAutoSave(command);
107
+ }
108
+ allow();
109
+ }
110
+
111
+ // ============================================================
112
+ // 1. Destructive Command Guard
113
+ // ============================================================
114
+ const DANGEROUS_PATTERNS = [
115
+ { pattern: /git\s+reset\s+--hard/i, name: 'git reset --hard', risk: 'Permanently discards all uncommitted changes', alternative: 'git stash or git reset --soft' },
116
+ { pattern: /git\s+clean\s+-[fd]/i, name: 'git clean -f/-fd', risk: 'Permanently deletes untracked files', alternative: 'git clean -n (dry run)' },
117
+ { pattern: /\brm\s+-[a-zA-Z]{0,10}(rf|fr)[a-zA-Z]{0,10}\b/i, name: 'rm -rf', risk: 'Recursively deletes files without confirmation', alternative: 'rm -ri (interactive) or move to trash' },
118
+ { pattern: /git\s+push\s+[^|]*--force(?!-with-lease)[^|]*(main|master)/i, name: 'git push --force to main/master', risk: 'Overwrites remote history', alternative: 'git push --force-with-lease' },
119
+ { pattern: /git\s+push\s+[^|]*(main|master)[^|]*--force(?!-with-lease)/i, name: 'git push --force to main/master', risk: 'Overwrites remote history', alternative: 'git push --force-with-lease' },
120
+ { pattern: /git\s+checkout\s+\.\s*$/i, name: 'git checkout .', risk: 'Discards all uncommitted changes', alternative: 'git stash or git diff' },
121
+ { pattern: /git\s+restore\s+\.\s*$/i, name: 'git restore .', risk: 'Discards all uncommitted changes', alternative: 'git stash or git diff' },
122
+ { pattern: /git\s+branch\s+-D/, name: 'git branch -D', risk: 'Force-deletes branch even if not merged', alternative: 'git branch -d (safe delete)' }
123
+ ];
124
+
125
+ const BYPASS_FILE = path.join(os.tmpdir(), '.claude-destructive-bypass.json');
126
+
127
+ function isSubagentActive() {
128
+ try {
129
+ const stackFile = path.join(os.tmpdir(), '.claude-agent-stack.json');
130
+ if (fs.existsSync(stackFile)) {
131
+ const stack = JSON.parse(fs.readFileSync(stackFile, 'utf8'));
132
+ return Array.isArray(stack.agents) && stack.agents.length > 0;
133
+ }
134
+ } catch {}
135
+ return false;
136
+ }
137
+
138
+ function checkBypass(command) {
139
+ try {
140
+ const bypass = JSON.parse(fs.readFileSync(BYPASS_FILE, 'utf8'));
141
+ if (typeof bypass.expiresAt !== 'number' || typeof bypass.command !== 'string') {
142
+ try { fs.unlinkSync(BYPASS_FILE); } catch {}
143
+ return false;
144
+ }
145
+ if (bypass.expiresAt <= Date.now()) {
146
+ try { fs.unlinkSync(BYPASS_FILE); } catch {}
147
+ return false;
148
+ }
149
+ if (bypass.command === command) {
150
+ try { fs.unlinkSync(BYPASS_FILE); } catch {}
151
+ return true;
152
+ }
153
+ } catch {}
154
+ return false;
155
+ }
156
+
157
+ function checkDestructive(command) {
158
+ if (isSubagentActive()) return;
159
+ if (command.includes('bash-guard') && command.includes('--bypass')) return;
160
+ if (checkBypass(command)) return;
161
+
162
+ for (const { pattern, name, risk, alternative } of DANGEROUS_PATTERNS) {
163
+ if (pattern.test(command)) {
164
+ block(`BLOCKED: Destructive Command Detected
165
+
166
+ **Command:** \`${name}\`
167
+ **Risk:** ${risk}
168
+
169
+ **USE THESE INSTEAD (no bypass needed):**
170
+ - Delete files individually: \`rm file1 file2\` (no -rf flag)
171
+ - Delete directory contents: \`find /path -mindepth 1 -delete\`
172
+ - Move to trash: \`mv /path ~/.Trash/\`
173
+ - Safe alternative: ${alternative}
174
+
175
+ **ONLY if user explicitly requests the destructive command:**
176
+ 1. Ask user with AskUserQuestion
177
+ 2. Run: Bash: node "${process.argv[1].replace(/'/g, "'\\''")}" --bypass '${command.replace(/'/g, "'\\''")}'
178
+ 3. Retry the original command`);
179
+ }
180
+ }
181
+ }
182
+
183
+ // ============================================================
184
+ // 2. MCP Server Guard
185
+ // ============================================================
186
+ const SERVER_START_PATTERNS = [
187
+ /npm run dev\b/, /npm run start\b/, /npm start\b/,
188
+ /tsx\s+.*src\/app\.ts/, /tsx\s+watch\s+.*src\/app\.ts/,
189
+ /node\s+.*src\/app\.ts/, /node\s+.*dist\/app\.js/,
190
+ /npx\s+tsx\s+.*src\/app/
191
+ ];
192
+
193
+ function checkMcpServer(command) {
194
+ if (!SERVER_START_PATTERNS.some(p => p.test(command))) return;
195
+
196
+ block(`MCP server commands are blocked. Tell the user to run manually:
197
+ cd ${projectDir} && npm run dev`);
198
+ }
199
+
200
+ // ============================================================
201
+ // 3. Workspace Pull Guard
202
+ // ============================================================
203
+ function checkWorkspacePull(command) {
204
+ if (!command.match(/npm\s+run\s+pull/)) return;
205
+ if (process.env.CLAUDE_AGENT_ID || process.env.CLAUDE_SUBAGENT) return;
206
+
207
+ const workspaceDir = path.join(projectDir, 'workspace');
208
+ if (!fs.existsSync(workspaceDir)) { invalidateSharedMemory(); return; }
209
+
210
+ try {
211
+ const result = spawnSync('git', ['status', '--porcelain', 'workspace/'], {
212
+ cwd: projectDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
213
+ });
214
+ const status = (result.stdout || '').trim();
215
+
216
+ if (status) {
217
+ const changedFiles = status.split('\n').map(l => l.trim()).filter(Boolean);
218
+ block(`BLOCKED: Uncommitted changes in workspace/
219
+
220
+ \`npm run pull\` will OVERWRITE your local TypeScript files!
221
+
222
+ **Changed files:**
223
+ ${changedFiles.map(f => ' ' + f).join('\n')}
224
+
225
+ Options:
226
+ 1. Push first: npm run push → npm run pull
227
+ 2. Save first: /save "WIP before pull" → npm run pull
228
+ 3. Discard: git checkout -- workspace/ → npm run pull`);
229
+ }
230
+ invalidateSharedMemory();
231
+ } catch {
232
+ invalidateSharedMemory();
233
+ }
234
+ }
235
+
236
+ // ============================================================
237
+ // 4. Workspace Auto-Save
238
+ // ============================================================
239
+ const PUSH_PATTERNS = [
240
+ /npm\s+run\s+.*push/, /npm\s+run\s+.*sync/,
241
+ /workflows-sync/, /fields-push/, /phases-push/,
242
+ /insights-push/, /templates-sync/
243
+ ];
244
+
245
+ function checkWorkspaceAutoSave(command) {
246
+ if (!PUSH_PATTERNS.some(p => p.test(command))) return;
247
+
248
+ const workspaceDir = path.join(projectDir, 'workspace');
249
+ if (!fs.existsSync(workspaceDir)) return;
250
+
251
+ try {
252
+ const result = spawnSync('git', ['status', '--porcelain', 'workspace/'], {
253
+ cwd: projectDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
254
+ });
255
+ const status = (result.stdout || '').trim();
256
+ if (!status) { console.error('[bash-guard] workspace/ clean, no auto-save needed'); return; }
257
+
258
+ const changes = status.split('\n').filter(Boolean).length;
259
+ const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ').replace(/[:.]/g, '-');
260
+ spawnSync('git', ['add', 'workspace/'], { cwd: projectDir, encoding: 'utf8' });
261
+ const commitResult = spawnSync('git', ['commit', '-m', `Auto-save workspace before push (${timestamp})`], {
262
+ cwd: projectDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
263
+ });
264
+ if (commitResult.status !== 0 && !commitResult.stderr?.includes('nothing to commit')) {
265
+ throw new Error(commitResult.stderr || 'git commit failed');
266
+ }
267
+ console.error('[bash-guard] Auto-saved ' + changes + ' workspace change(s) before push');
268
+ } catch (err) {
269
+ console.error('[bash-guard] Could not auto-save: ' + err.message);
270
+ }
271
+ }
272
+
273
+ // ============================================================
274
+ // Shared memory invalidation (after pull)
275
+ // ============================================================
276
+ function invalidateSharedMemory() {
277
+ try {
278
+ const mem = require('./_shared-memory.cjs');
279
+ mem.invalidate();
280
+ console.error('[bash-guard] Shared memory invalidated (workspace pull).');
281
+ } catch {}
282
+ }