@hasna/hooks 0.0.1 → 0.0.2

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 (62) hide show
  1. package/dist/index.js +366 -0
  2. package/hooks/hook-agentmessages/bin/cli.ts +125 -0
  3. package/package.json +2 -2
  4. package/hooks/hook-agentmessages/src/check-messages.ts +0 -151
  5. package/hooks/hook-agentmessages/src/install.ts +0 -126
  6. package/hooks/hook-agentmessages/src/session-start.ts +0 -255
  7. package/hooks/hook-agentmessages/src/uninstall.ts +0 -89
  8. package/hooks/hook-branchprotect/src/cli.ts +0 -126
  9. package/hooks/hook-branchprotect/src/hook.ts +0 -88
  10. package/hooks/hook-branchprotect/tsconfig.json +0 -25
  11. package/hooks/hook-checkbugs/src/cli.ts +0 -628
  12. package/hooks/hook-checkbugs/src/hook.ts +0 -335
  13. package/hooks/hook-checkbugs/tsconfig.json +0 -15
  14. package/hooks/hook-checkdocs/src/cli.ts +0 -628
  15. package/hooks/hook-checkdocs/src/hook.ts +0 -310
  16. package/hooks/hook-checkdocs/tsconfig.json +0 -15
  17. package/hooks/hook-checkfiles/src/cli.ts +0 -545
  18. package/hooks/hook-checkfiles/src/hook.ts +0 -321
  19. package/hooks/hook-checkfiles/tsconfig.json +0 -15
  20. package/hooks/hook-checklint/src/cli-patch.ts +0 -32
  21. package/hooks/hook-checklint/src/cli.ts +0 -667
  22. package/hooks/hook-checklint/src/hook.ts +0 -473
  23. package/hooks/hook-checklint/tsconfig.json +0 -15
  24. package/hooks/hook-checkpoint/src/cli.ts +0 -191
  25. package/hooks/hook-checkpoint/src/hook.ts +0 -207
  26. package/hooks/hook-checkpoint/tsconfig.json +0 -25
  27. package/hooks/hook-checksecurity/src/cli.ts +0 -601
  28. package/hooks/hook-checksecurity/src/hook.ts +0 -334
  29. package/hooks/hook-checksecurity/tsconfig.json +0 -15
  30. package/hooks/hook-checktasks/src/cli.ts +0 -578
  31. package/hooks/hook-checktasks/src/hook.ts +0 -308
  32. package/hooks/hook-checktasks/tsconfig.json +0 -20
  33. package/hooks/hook-checktests/src/cli.ts +0 -627
  34. package/hooks/hook-checktests/src/hook.ts +0 -334
  35. package/hooks/hook-checktests/tsconfig.json +0 -15
  36. package/hooks/hook-contextrefresh/src/cli.ts +0 -152
  37. package/hooks/hook-contextrefresh/src/hook.ts +0 -148
  38. package/hooks/hook-contextrefresh/tsconfig.json +0 -25
  39. package/hooks/hook-gitguard/src/cli.ts +0 -159
  40. package/hooks/hook-gitguard/src/hook.ts +0 -129
  41. package/hooks/hook-gitguard/tsconfig.json +0 -25
  42. package/hooks/hook-packageage/src/cli.ts +0 -165
  43. package/hooks/hook-packageage/src/hook.ts +0 -177
  44. package/hooks/hook-packageage/tsconfig.json +0 -25
  45. package/hooks/hook-phonenotify/src/cli.ts +0 -196
  46. package/hooks/hook-phonenotify/src/hook.ts +0 -139
  47. package/hooks/hook-phonenotify/tsconfig.json +0 -25
  48. package/hooks/hook-precompact/src/cli.ts +0 -168
  49. package/hooks/hook-precompact/src/hook.ts +0 -122
  50. package/hooks/hook-precompact/tsconfig.json +0 -25
  51. package/src/cli/components/App.tsx +0 -191
  52. package/src/cli/components/CategorySelect.tsx +0 -37
  53. package/src/cli/components/DataTable.tsx +0 -133
  54. package/src/cli/components/Header.tsx +0 -18
  55. package/src/cli/components/HookSelect.tsx +0 -29
  56. package/src/cli/components/InstallProgress.tsx +0 -105
  57. package/src/cli/components/SearchView.tsx +0 -86
  58. package/src/cli/index.tsx +0 -218
  59. package/src/index.ts +0 -31
  60. package/src/lib/installer.ts +0 -288
  61. package/src/lib/registry.ts +0 -205
  62. package/tsconfig.json +0 -17
package/dist/index.js ADDED
@@ -0,0 +1,366 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/lib/registry.ts
5
+ var CATEGORIES = [
6
+ "Git Safety",
7
+ "Code Quality",
8
+ "Security",
9
+ "Notifications",
10
+ "Context Management"
11
+ ];
12
+ var HOOKS = [
13
+ {
14
+ name: "gitguard",
15
+ displayName: "Git Guard",
16
+ description: "Blocks destructive git operations like reset --hard, push --force, clean -f",
17
+ version: "0.1.0",
18
+ category: "Git Safety",
19
+ event: "PreToolUse",
20
+ matcher: "Bash",
21
+ tags: ["git", "safety", "destructive", "guard"]
22
+ },
23
+ {
24
+ name: "branchprotect",
25
+ displayName: "Branch Protect",
26
+ description: "Prevents editing files directly on main/master branch",
27
+ version: "0.1.0",
28
+ category: "Git Safety",
29
+ event: "PreToolUse",
30
+ matcher: "Write|Edit|NotebookEdit",
31
+ tags: ["git", "branch", "protection", "main"]
32
+ },
33
+ {
34
+ name: "checkpoint",
35
+ displayName: "Checkpoint",
36
+ description: "Creates shadow git snapshots before file modifications for easy rollback",
37
+ version: "0.1.0",
38
+ category: "Git Safety",
39
+ event: "PreToolUse",
40
+ matcher: "Write|Edit|NotebookEdit",
41
+ tags: ["git", "snapshot", "rollback", "backup"]
42
+ },
43
+ {
44
+ name: "checktests",
45
+ displayName: "Check Tests",
46
+ description: "Checks for missing tests after file edits",
47
+ version: "0.1.6",
48
+ category: "Code Quality",
49
+ event: "PostToolUse",
50
+ matcher: "Edit|Write|NotebookEdit",
51
+ tags: ["tests", "coverage", "quality"]
52
+ },
53
+ {
54
+ name: "checklint",
55
+ displayName: "Check Lint",
56
+ description: "Runs linting after file edits and creates tasks for errors",
57
+ version: "0.1.7",
58
+ category: "Code Quality",
59
+ event: "PostToolUse",
60
+ matcher: "Edit|Write|NotebookEdit",
61
+ tags: ["lint", "style", "quality"]
62
+ },
63
+ {
64
+ name: "checkfiles",
65
+ displayName: "Check Files",
66
+ description: "Runs headless agent to review files and create tasks",
67
+ version: "0.1.4",
68
+ category: "Code Quality",
69
+ event: "PostToolUse",
70
+ matcher: "Edit|Write|NotebookEdit",
71
+ tags: ["review", "files", "quality"]
72
+ },
73
+ {
74
+ name: "checkbugs",
75
+ displayName: "Check Bugs",
76
+ description: "Checks for bugs via Codex headless agent",
77
+ version: "0.1.6",
78
+ category: "Code Quality",
79
+ event: "PostToolUse",
80
+ matcher: "Edit|Write|NotebookEdit",
81
+ tags: ["bugs", "analysis", "quality"]
82
+ },
83
+ {
84
+ name: "checkdocs",
85
+ displayName: "Check Docs",
86
+ description: "Checks for missing documentation and creates tasks",
87
+ version: "0.2.1",
88
+ category: "Code Quality",
89
+ event: "PostToolUse",
90
+ matcher: "Edit|Write|NotebookEdit",
91
+ tags: ["docs", "documentation", "quality"]
92
+ },
93
+ {
94
+ name: "checktasks",
95
+ displayName: "Check Tasks",
96
+ description: "Validates task completion and tracks progress",
97
+ version: "1.0.8",
98
+ category: "Code Quality",
99
+ event: "PostToolUse",
100
+ matcher: "Edit|Write|NotebookEdit",
101
+ tags: ["tasks", "tracking", "quality"]
102
+ },
103
+ {
104
+ name: "checksecurity",
105
+ displayName: "Check Security",
106
+ description: "Runs security checks via Claude and Codex headless agents",
107
+ version: "0.1.6",
108
+ category: "Security",
109
+ event: "PostToolUse",
110
+ matcher: "Edit|Write|NotebookEdit",
111
+ tags: ["security", "audit", "vulnerabilities"]
112
+ },
113
+ {
114
+ name: "packageage",
115
+ displayName: "Package Age",
116
+ description: "Checks package age before install to prevent typosquatting",
117
+ version: "0.1.1",
118
+ category: "Security",
119
+ event: "PreToolUse",
120
+ matcher: "Bash",
121
+ tags: ["npm", "packages", "typosquatting", "supply-chain"]
122
+ },
123
+ {
124
+ name: "phonenotify",
125
+ displayName: "Phone Notify",
126
+ description: "Sends push notifications to phone via ntfy.sh",
127
+ version: "0.1.0",
128
+ category: "Notifications",
129
+ event: "Stop",
130
+ matcher: "",
131
+ tags: ["notification", "phone", "push", "ntfy"]
132
+ },
133
+ {
134
+ name: "agentmessages",
135
+ displayName: "Agent Messages",
136
+ description: "Inter-agent messaging integration for service-message",
137
+ version: "0.1.0",
138
+ category: "Notifications",
139
+ event: "Stop",
140
+ matcher: "",
141
+ tags: ["messaging", "agents", "inter-agent"]
142
+ },
143
+ {
144
+ name: "contextrefresh",
145
+ displayName: "Context Refresh",
146
+ description: "Re-injects important context every N prompts to prevent drift",
147
+ version: "0.1.0",
148
+ category: "Context Management",
149
+ event: "Notification",
150
+ matcher: "",
151
+ tags: ["context", "memory", "prompts", "refresh"]
152
+ },
153
+ {
154
+ name: "precompact",
155
+ displayName: "Pre-Compact",
156
+ description: "Saves session state before context compaction",
157
+ version: "0.1.0",
158
+ category: "Context Management",
159
+ event: "Notification",
160
+ matcher: "",
161
+ tags: ["context", "compaction", "state", "backup"]
162
+ }
163
+ ];
164
+ function getHooksByCategory(category) {
165
+ return HOOKS.filter((h) => h.category === category);
166
+ }
167
+ function searchHooks(query) {
168
+ const q = query.toLowerCase();
169
+ return HOOKS.filter((h) => h.name.toLowerCase().includes(q) || h.displayName.toLowerCase().includes(q) || h.description.toLowerCase().includes(q) || h.tags.some((t) => t.includes(q)));
170
+ }
171
+ function getHook(name) {
172
+ return HOOKS.find((h) => h.name === name);
173
+ }
174
+ // src/lib/installer.ts
175
+ import { existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
176
+ import { join, dirname } from "path";
177
+ import { homedir } from "os";
178
+ import { fileURLToPath } from "url";
179
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
180
+ var HOOKS_DIR = join(__dirname2, "..", "..", "hooks");
181
+ var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
182
+ function getHookPath(name) {
183
+ const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
184
+ return join(HOOKS_DIR, hookName);
185
+ }
186
+ function hookExists(name) {
187
+ return existsSync(getHookPath(name));
188
+ }
189
+ function readSettings() {
190
+ try {
191
+ if (existsSync(SETTINGS_PATH)) {
192
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
193
+ }
194
+ } catch {}
195
+ return {};
196
+ }
197
+ function writeSettings(settings) {
198
+ const dir = dirname(SETTINGS_PATH);
199
+ if (!existsSync(dir)) {
200
+ mkdirSync(dir, { recursive: true });
201
+ }
202
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + `
203
+ `);
204
+ }
205
+ function installHook(name, options = {}) {
206
+ const { targetDir = process.cwd(), overwrite = false } = options;
207
+ const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
208
+ const shortName = hookName.replace("hook-", "");
209
+ const sourcePath = getHookPath(name);
210
+ const destDir = join(targetDir, ".hooks");
211
+ const destPath = join(destDir, hookName);
212
+ if (!existsSync(sourcePath)) {
213
+ return {
214
+ hook: shortName,
215
+ success: false,
216
+ error: `Hook '${shortName}' not found`
217
+ };
218
+ }
219
+ if (existsSync(destPath) && !overwrite) {
220
+ return {
221
+ hook: shortName,
222
+ success: false,
223
+ error: `Already installed. Use --overwrite to replace.`,
224
+ path: destPath
225
+ };
226
+ }
227
+ try {
228
+ if (!existsSync(destDir)) {
229
+ mkdirSync(destDir, { recursive: true });
230
+ }
231
+ cpSync(sourcePath, destPath, { recursive: true });
232
+ registerHookInSettings(shortName);
233
+ updateHooksIndex(destDir);
234
+ return {
235
+ hook: shortName,
236
+ success: true,
237
+ path: destPath
238
+ };
239
+ } catch (error) {
240
+ return {
241
+ hook: shortName,
242
+ success: false,
243
+ error: error instanceof Error ? error.message : "Unknown error"
244
+ };
245
+ }
246
+ }
247
+ function registerHookInSettings(name) {
248
+ const meta = getHook(name);
249
+ if (!meta)
250
+ return;
251
+ const settings = readSettings();
252
+ if (!settings.hooks)
253
+ settings.hooks = {};
254
+ const eventKey = meta.event;
255
+ if (!settings.hooks[eventKey])
256
+ settings.hooks[eventKey] = [];
257
+ const hookCommand = `hook-${name}`;
258
+ const existing = settings.hooks[eventKey].find((entry2) => entry2.hooks?.some((h) => h.command?.includes(hookCommand)));
259
+ if (existing)
260
+ return;
261
+ const entry = {
262
+ hooks: [{ type: "command", command: hookCommand }]
263
+ };
264
+ if (meta.matcher) {
265
+ entry.matcher = meta.matcher;
266
+ }
267
+ settings.hooks[eventKey].push(entry);
268
+ writeSettings(settings);
269
+ }
270
+ function unregisterHookFromSettings(name) {
271
+ const meta = getHook(name);
272
+ if (!meta)
273
+ return;
274
+ const settings = readSettings();
275
+ if (!settings.hooks)
276
+ return;
277
+ const eventKey = meta.event;
278
+ if (!settings.hooks[eventKey])
279
+ return;
280
+ const hookCommand = `hook-${name}`;
281
+ settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command?.includes(hookCommand)));
282
+ if (settings.hooks[eventKey].length === 0) {
283
+ delete settings.hooks[eventKey];
284
+ }
285
+ if (Object.keys(settings.hooks).length === 0) {
286
+ delete settings.hooks;
287
+ }
288
+ writeSettings(settings);
289
+ }
290
+ function installHooks(names, options = {}) {
291
+ return names.map((name) => installHook(name, options));
292
+ }
293
+ function updateHooksIndex(hooksDir) {
294
+ const indexPath = join(hooksDir, "index.ts");
295
+ const { readdirSync } = __require("fs");
296
+ const hooks = readdirSync(hooksDir).filter((f) => f.startsWith("hook-") && !f.includes("."));
297
+ const exports = hooks.map((h) => {
298
+ const name = h.replace("hook-", "");
299
+ return `export * as ${name} from './${h}/src/index.js';`;
300
+ }).join(`
301
+ `);
302
+ const content = `/**
303
+ * Auto-generated index of installed hooks
304
+ * Do not edit manually - run 'hooks install' to update
305
+ */
306
+
307
+ ${exports}
308
+ `;
309
+ writeFileSync(indexPath, content);
310
+ }
311
+ function getInstalledHooks(targetDir = process.cwd()) {
312
+ const hooksDir = join(targetDir, ".hooks");
313
+ if (!existsSync(hooksDir)) {
314
+ return [];
315
+ }
316
+ const { readdirSync, statSync } = __require("fs");
317
+ return readdirSync(hooksDir).filter((f) => {
318
+ const fullPath = join(hooksDir, f);
319
+ return f.startsWith("hook-") && statSync(fullPath).isDirectory();
320
+ }).map((f) => f.replace("hook-", ""));
321
+ }
322
+ function getRegisteredHooks() {
323
+ const settings = readSettings();
324
+ if (!settings.hooks)
325
+ return [];
326
+ const registered = [];
327
+ for (const eventKey of Object.keys(settings.hooks)) {
328
+ for (const entry of settings.hooks[eventKey]) {
329
+ for (const hook of entry.hooks || []) {
330
+ const match = hook.command?.match(/hook-(\w+)/);
331
+ if (match) {
332
+ registered.push(match[1]);
333
+ }
334
+ }
335
+ }
336
+ }
337
+ return [...new Set(registered)];
338
+ }
339
+ function removeHook(name, targetDir = process.cwd()) {
340
+ const { rmSync } = __require("fs");
341
+ const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
342
+ const shortName = hookName.replace("hook-", "");
343
+ const hooksDir = join(targetDir, ".hooks");
344
+ const hookPath = join(hooksDir, hookName);
345
+ if (!existsSync(hookPath)) {
346
+ return false;
347
+ }
348
+ rmSync(hookPath, { recursive: true });
349
+ unregisterHookFromSettings(shortName);
350
+ updateHooksIndex(hooksDir);
351
+ return true;
352
+ }
353
+ export {
354
+ searchHooks,
355
+ removeHook,
356
+ installHooks,
357
+ installHook,
358
+ hookExists,
359
+ getRegisteredHooks,
360
+ getInstalledHooks,
361
+ getHooksByCategory,
362
+ getHookPath,
363
+ getHook,
364
+ HOOKS,
365
+ CATEGORIES
366
+ };
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * CLI for hook-agentmessages
4
+ *
5
+ * Usage:
6
+ * hook-agentmessages install - Install hooks into Claude Code
7
+ * hook-agentmessages uninstall - Remove hooks from Claude Code
8
+ * hook-agentmessages status - Show hook status
9
+ */
10
+
11
+ import { homedir } from 'os';
12
+ import { join } from 'path';
13
+
14
+ const CLAUDE_SETTINGS_FILE = join(homedir(), '.claude', 'settings.json');
15
+
16
+ const args = process.argv.slice(2);
17
+ const command = args[0];
18
+
19
+ async function showStatus() {
20
+ console.log('hook-agentmessages status\n');
21
+
22
+ // Check if hooks are installed
23
+ try {
24
+ const file = Bun.file(CLAUDE_SETTINGS_FILE);
25
+ if (await file.exists()) {
26
+ const settings = await file.json();
27
+
28
+ if (settings.hooks) {
29
+ let installed = false;
30
+
31
+ if (settings.hooks.SessionStart) {
32
+ const hasHook = settings.hooks.SessionStart.some((h: any) =>
33
+ h.hooks?.some((hook: any) => hook.command?.includes('hook-agentmessages'))
34
+ );
35
+ if (hasHook) {
36
+ console.log(' SessionStart hook: installed');
37
+ installed = true;
38
+ }
39
+ }
40
+
41
+ if (settings.hooks.Stop) {
42
+ const hasHook = settings.hooks.Stop.some((h: any) =>
43
+ h.hooks?.some((hook: any) => hook.command?.includes('hook-agentmessages'))
44
+ );
45
+ if (hasHook) {
46
+ console.log(' Stop hook: installed');
47
+ installed = true;
48
+ }
49
+ }
50
+
51
+ if (!installed) {
52
+ console.log(' No hook-agentmessages hooks installed');
53
+ }
54
+ } else {
55
+ console.log(' No hooks configured in Claude Code');
56
+ }
57
+ } else {
58
+ console.log(' Claude Code settings not found');
59
+ }
60
+ } catch (err) {
61
+ console.log(' Error reading settings:', (err as Error).message);
62
+ }
63
+
64
+ // Check service-message status
65
+ const serviceDir = join(homedir(), '.service', 'service-message');
66
+ const configFile = Bun.file(join(serviceDir, 'config.json'));
67
+
68
+ console.log('\nservice-message integration:');
69
+ if (await configFile.exists()) {
70
+ const config = await configFile.json();
71
+ console.log(` Agent ID: ${config.agentId || 'not set'}`);
72
+ console.log(` Data dir: ${serviceDir}`);
73
+ } else {
74
+ console.log(' Not configured');
75
+ }
76
+ }
77
+
78
+ function showHelp() {
79
+ console.log(`
80
+ hook-agentmessages - Claude Code hook for service-message integration
81
+
82
+ Usage:
83
+ hook-agentmessages <command>
84
+
85
+ Commands:
86
+ install Install hooks into Claude Code settings
87
+ uninstall Remove hooks from Claude Code settings
88
+ status Show current hook status
89
+
90
+ Examples:
91
+ hook-agentmessages install
92
+ hook-agentmessages status
93
+ `);
94
+ }
95
+
96
+ async function main() {
97
+ if (!command || command === '--help' || command === '-h') {
98
+ showHelp();
99
+ return;
100
+ }
101
+
102
+ switch (command) {
103
+ case 'install':
104
+ await import('../src/install.ts');
105
+ break;
106
+
107
+ case 'uninstall':
108
+ await import('../src/uninstall.ts');
109
+ break;
110
+
111
+ case 'status':
112
+ await showStatus();
113
+ break;
114
+
115
+ default:
116
+ console.error(`Unknown command: ${command}`);
117
+ showHelp();
118
+ process.exit(1);
119
+ }
120
+ }
121
+
122
+ main().catch((err) => {
123
+ console.error('Error:', err.message);
124
+ process.exit(1);
125
+ });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Open source Claude Code hooks library - Install hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {
7
- "hooks": "./bin/index.js"
7
+ "hooks": "bin/index.js"
8
8
  },
9
9
  "exports": {
10
10
  ".": {
@@ -1,151 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Stop hook for service-message
4
- *
5
- * Runs when Claude finishes a response. Checks for unread messages
6
- * and notifies Claude if there are any pending messages.
7
- *
8
- * This is efficient because it only runs after Claude completes a turn,
9
- * not continuously polling.
10
- */
11
-
12
- import { homedir } from 'os';
13
- import { join } from 'path';
14
- import { readdir } from 'fs/promises';
15
-
16
- interface HookInput {
17
- session_id: string;
18
- cwd: string;
19
- hook_event_name: string;
20
- stop_hook_active?: boolean;
21
- }
22
-
23
- interface Message {
24
- id: string;
25
- timestamp: number;
26
- from: string;
27
- to: string;
28
- project: string;
29
- subject: string;
30
- body: string;
31
- read?: boolean;
32
- }
33
-
34
- const SERVICE_DIR = join(homedir(), '.service', 'service-message');
35
-
36
- /**
37
- * Sanitize ID to prevent path traversal attacks
38
- */
39
- function sanitizeId(id: string): string | null {
40
- if (!id || typeof id !== 'string') return null;
41
- // Only allow alphanumeric, dash, underscore
42
- if (!/^[a-zA-Z0-9_-]+$/.test(id)) return null;
43
- // Reject path traversal attempts
44
- if (id.includes('..') || id.includes('/') || id.includes('\\')) return null;
45
- return id;
46
- }
47
-
48
- async function readJson<T>(path: string): Promise<T | null> {
49
- try {
50
- const file = Bun.file(path);
51
- if (await file.exists()) {
52
- return await file.json();
53
- }
54
- } catch {}
55
- return null;
56
- }
57
-
58
- async function getUnreadMessages(agentId: string, projectId?: string): Promise<Message[]> {
59
- const messages: Message[] = [];
60
- const messagesDir = join(SERVICE_DIR, 'messages');
61
-
62
- try {
63
- const rawProjects = projectId ? [projectId] : await readdir(messagesDir);
64
- // Sanitize all project names to prevent path traversal
65
- const projects = rawProjects.map(p => sanitizeId(p)).filter((p): p is string => p !== null);
66
-
67
- for (const proj of projects) {
68
- // Check inbox
69
- const inboxDir = join(messagesDir, proj, 'inbox', agentId);
70
- try {
71
- const files = await readdir(inboxDir);
72
- for (const file of files) {
73
- if (!file.endsWith('.json')) continue;
74
- // Sanitize filename to prevent path traversal
75
- const safeFile = sanitizeId(file.replace('.json', ''));
76
- if (!safeFile) continue;
77
- const msg = await readJson<Message>(join(inboxDir, `${safeFile}.json`));
78
- if (msg && !msg.read) {
79
- messages.push(msg);
80
- }
81
- }
82
- } catch {}
83
-
84
- // Check broadcast
85
- const broadcastDir = join(messagesDir, proj, 'broadcast');
86
- try {
87
- const files = await readdir(broadcastDir);
88
- for (const file of files) {
89
- if (!file.endsWith('.json')) continue;
90
- // Sanitize filename to prevent path traversal
91
- const safeFile = sanitizeId(file.replace('.json', ''));
92
- if (!safeFile) continue;
93
- const msg = await readJson<Message>(join(broadcastDir, `${safeFile}.json`));
94
- if (msg && !msg.read && msg.from !== agentId) {
95
- messages.push(msg);
96
- }
97
- }
98
- } catch {}
99
- }
100
- } catch {}
101
-
102
- return messages.sort((a, b) => b.timestamp - a.timestamp);
103
- }
104
-
105
- async function main() {
106
- // Get agent ID from environment (set by session-start hook)
107
- const rawAgentId = process.env.SMSG_AGENT_ID;
108
- const rawProjectId = process.env.SMSG_PROJECT_ID;
109
-
110
- // Sanitize IDs to prevent path traversal
111
- const agentId = rawAgentId ? sanitizeId(rawAgentId) : null;
112
- const projectId = rawProjectId ? sanitizeId(rawProjectId) : undefined;
113
-
114
- if (!agentId) {
115
- // Agent not configured or invalid, skip silently
116
- console.log(JSON.stringify({ continue: true }));
117
- return;
118
- }
119
-
120
- // Check for unread messages
121
- const unreadMessages = await getUnreadMessages(agentId, projectId);
122
-
123
- if (unreadMessages.length === 0) {
124
- // No messages, continue normally
125
- console.log(JSON.stringify({ continue: true }));
126
- return;
127
- }
128
-
129
- // Format message summary in a friendly way
130
- const msgList = unreadMessages.slice(0, 3).map(msg => {
131
- const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
132
- const preview = msg.body.slice(0, 60).replace(/\n/g, ' ');
133
- return `šŸ“Ø **${msg.subject}** from \`${msg.from}\` (${time})\n ${preview}${msg.body.length > 60 ? '...' : ''}`;
134
- }).join('\n\n');
135
-
136
- const moreNote = unreadMessages.length > 3
137
- ? `\n\n_...and ${unreadMessages.length - 3} more message(s)_`
138
- : '';
139
-
140
- // Inject message in a friendly, readable format
141
- console.log(JSON.stringify({
142
- continue: true,
143
- stopReason: `šŸ“¬ **You have ${unreadMessages.length} unread message(s):**\n\n${msgList}${moreNote}\n\nšŸ’” Use \`service-message inbox\` to see all messages or \`service-message read <id>\` to read one.`
144
- }));
145
- }
146
-
147
- main().catch(() => {
148
- // Don't block on errors
149
- console.log(JSON.stringify({ continue: true }));
150
- process.exit(0);
151
- });