@stackmemoryai/stackmemory 0.5.3 → 0.5.5

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 (37) hide show
  1. package/bin/claude-sm +6 -0
  2. package/bin/claude-smd +6 -0
  3. package/dist/cli/claude-sm-danger.js +20 -0
  4. package/dist/cli/claude-sm-danger.js.map +7 -0
  5. package/dist/cli/commands/api.js +228 -0
  6. package/dist/cli/commands/api.js.map +7 -0
  7. package/dist/cli/commands/cleanup-processes.js +64 -0
  8. package/dist/cli/commands/cleanup-processes.js.map +7 -0
  9. package/dist/cli/commands/hooks.js +294 -0
  10. package/dist/cli/commands/hooks.js.map +7 -0
  11. package/dist/cli/commands/shell.js +248 -0
  12. package/dist/cli/commands/shell.js.map +7 -0
  13. package/dist/cli/commands/sweep.js +173 -5
  14. package/dist/cli/commands/sweep.js.map +3 -3
  15. package/dist/cli/index.js +9 -1
  16. package/dist/cli/index.js.map +2 -2
  17. package/dist/hooks/config.js +146 -0
  18. package/dist/hooks/config.js.map +7 -0
  19. package/dist/hooks/daemon.js +360 -0
  20. package/dist/hooks/daemon.js.map +7 -0
  21. package/dist/hooks/events.js +51 -0
  22. package/dist/hooks/events.js.map +7 -0
  23. package/dist/hooks/index.js +4 -0
  24. package/dist/hooks/index.js.map +7 -0
  25. package/dist/skills/api-discovery.js +349 -0
  26. package/dist/skills/api-discovery.js.map +7 -0
  27. package/dist/skills/api-skill.js +471 -0
  28. package/dist/skills/api-skill.js.map +7 -0
  29. package/dist/skills/claude-skills.js +49 -1
  30. package/dist/skills/claude-skills.js.map +2 -2
  31. package/dist/utils/process-cleanup.js +132 -0
  32. package/dist/utils/process-cleanup.js.map +7 -0
  33. package/package.json +4 -2
  34. package/scripts/install-sweep-hook.sh +89 -0
  35. package/templates/claude-hooks/post-edit-sweep.js +437 -0
  36. package/templates/shell/sweep-complete.zsh +116 -0
  37. package/templates/shell/sweep-suggest.js +161 -0
@@ -0,0 +1,146 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ const DEFAULT_CONFIG = {
4
+ version: "1.0.0",
5
+ daemon: {
6
+ enabled: true,
7
+ log_level: "info",
8
+ pid_file: join(process.env.HOME || "/tmp", ".stackmemory", "hooks.pid"),
9
+ log_file: join(process.env.HOME || "/tmp", ".stackmemory", "hooks.log")
10
+ },
11
+ file_watch: {
12
+ enabled: true,
13
+ paths: ["."],
14
+ ignore: ["node_modules", ".git", "dist", "build", ".next", "__pycache__"],
15
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java"]
16
+ },
17
+ hooks: {
18
+ file_change: {
19
+ enabled: true,
20
+ handler: "sweep-predict",
21
+ output: "log",
22
+ debounce_ms: 2e3,
23
+ cooldown_ms: 1e4
24
+ },
25
+ session_start: {
26
+ enabled: true,
27
+ handler: "context-load",
28
+ output: "silent"
29
+ },
30
+ suggestion_ready: {
31
+ enabled: true,
32
+ handler: "display-suggestion",
33
+ output: "overlay"
34
+ }
35
+ }
36
+ };
37
+ function getConfigPath() {
38
+ return join(process.env.HOME || "/tmp", ".stackmemory", "hooks.yaml");
39
+ }
40
+ function loadConfig() {
41
+ const configPath = getConfigPath();
42
+ if (!existsSync(configPath)) {
43
+ return DEFAULT_CONFIG;
44
+ }
45
+ try {
46
+ const content = readFileSync(configPath, "utf-8");
47
+ const parsed = parseYaml(content);
48
+ return mergeConfig(DEFAULT_CONFIG, parsed);
49
+ } catch {
50
+ return DEFAULT_CONFIG;
51
+ }
52
+ }
53
+ function saveConfig(config) {
54
+ const configPath = getConfigPath();
55
+ const dir = dirname(configPath);
56
+ if (!existsSync(dir)) {
57
+ mkdirSync(dir, { recursive: true });
58
+ }
59
+ const yaml = toYaml(config);
60
+ writeFileSync(configPath, yaml);
61
+ }
62
+ function initConfig() {
63
+ const configPath = getConfigPath();
64
+ if (existsSync(configPath)) {
65
+ return loadConfig();
66
+ }
67
+ saveConfig(DEFAULT_CONFIG);
68
+ return DEFAULT_CONFIG;
69
+ }
70
+ function parseYaml(content) {
71
+ const result = {};
72
+ const lines = content.split("\n");
73
+ const stack = [
74
+ { indent: -1, obj: result }
75
+ ];
76
+ for (const line of lines) {
77
+ if (!line.trim() || line.trim().startsWith("#")) continue;
78
+ const indent = line.search(/\S/);
79
+ const trimmed = line.trim();
80
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
81
+ stack.pop();
82
+ }
83
+ const colonIdx = trimmed.indexOf(":");
84
+ if (colonIdx === -1) continue;
85
+ const key = trimmed.slice(0, colonIdx).trim();
86
+ const value = trimmed.slice(colonIdx + 1).trim();
87
+ const current = stack[stack.length - 1].obj;
88
+ if (value === "" || value === "|") {
89
+ current[key] = {};
90
+ stack.push({ indent, obj: current[key] });
91
+ } else if (value.startsWith("[") && value.endsWith("]")) {
92
+ current[key] = value.slice(1, -1).split(",").map((s) => s.trim().replace(/['"]/g, ""));
93
+ } else if (value === "true") {
94
+ current[key] = true;
95
+ } else if (value === "false") {
96
+ current[key] = false;
97
+ } else if (/^\d+$/.test(value)) {
98
+ current[key] = parseInt(value, 10);
99
+ } else {
100
+ current[key] = value.replace(/['"]/g, "");
101
+ }
102
+ }
103
+ return result;
104
+ }
105
+ function toYaml(obj, indent = 0) {
106
+ const spaces = " ".repeat(indent);
107
+ let result = "";
108
+ if (Array.isArray(obj)) {
109
+ result += `[${obj.map((v) => typeof v === "string" ? `'${v}'` : v).join(", ")}]
110
+ `;
111
+ } else if (typeof obj === "object" && obj !== null) {
112
+ for (const [key, value] of Object.entries(obj)) {
113
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
114
+ result += `${spaces}${key}:
115
+ ${toYaml(value, indent + 1)}`;
116
+ } else {
117
+ result += `${spaces}${key}: ${toYaml(value, indent)}`;
118
+ }
119
+ }
120
+ } else if (typeof obj === "string") {
121
+ result += `${obj}
122
+ `;
123
+ } else if (typeof obj === "boolean" || typeof obj === "number") {
124
+ result += `${obj}
125
+ `;
126
+ } else {
127
+ result += "\n";
128
+ }
129
+ return result;
130
+ }
131
+ function mergeConfig(defaults, overrides) {
132
+ return {
133
+ ...defaults,
134
+ ...overrides,
135
+ daemon: { ...defaults.daemon, ...overrides.daemon || {} },
136
+ file_watch: { ...defaults.file_watch, ...overrides.file_watch || {} },
137
+ hooks: { ...defaults.hooks, ...overrides.hooks || {} }
138
+ };
139
+ }
140
+ export {
141
+ getConfigPath,
142
+ initConfig,
143
+ loadConfig,
144
+ saveConfig
145
+ };
146
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/config.ts"],
4
+ "sourcesContent": ["/**\n * StackMemory Hook Configuration\n * Loads and manages hook configuration\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { HookEventType } from './events.js';\n\nexport type OutputType =\n | 'overlay'\n | 'notification'\n | 'log'\n | 'prepend'\n | 'silent';\n\nexport interface HookConfig {\n enabled: boolean;\n handler: string;\n output: OutputType;\n delay_ms?: number;\n debounce_ms?: number;\n cooldown_ms?: number;\n options?: Record<string, unknown>;\n}\n\nexport interface HooksConfig {\n version: string;\n daemon: {\n enabled: boolean;\n log_level: 'debug' | 'info' | 'warn' | 'error';\n pid_file: string;\n log_file: string;\n };\n file_watch: {\n enabled: boolean;\n paths: string[];\n ignore: string[];\n extensions: string[];\n };\n hooks: Partial<Record<HookEventType, HookConfig>>;\n}\n\nconst DEFAULT_CONFIG: HooksConfig = {\n version: '1.0.0',\n daemon: {\n enabled: true,\n log_level: 'info',\n pid_file: join(process.env.HOME || '/tmp', '.stackmemory', 'hooks.pid'),\n log_file: join(process.env.HOME || '/tmp', '.stackmemory', 'hooks.log'),\n },\n file_watch: {\n enabled: true,\n paths: ['.'],\n ignore: ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__'],\n extensions: ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java'],\n },\n hooks: {\n file_change: {\n enabled: true,\n handler: 'sweep-predict',\n output: 'log',\n debounce_ms: 2000,\n cooldown_ms: 10000,\n },\n session_start: {\n enabled: true,\n handler: 'context-load',\n output: 'silent',\n },\n suggestion_ready: {\n enabled: true,\n handler: 'display-suggestion',\n output: 'overlay',\n },\n },\n};\n\nexport function getConfigPath(): string {\n return join(process.env.HOME || '/tmp', '.stackmemory', 'hooks.yaml');\n}\n\nexport function loadConfig(): HooksConfig {\n const configPath = getConfigPath();\n\n if (!existsSync(configPath)) {\n return DEFAULT_CONFIG;\n }\n\n try {\n const content = readFileSync(configPath, 'utf-8');\n const parsed = parseYaml(content);\n return mergeConfig(DEFAULT_CONFIG, parsed);\n } catch {\n return DEFAULT_CONFIG;\n }\n}\n\nexport function saveConfig(config: HooksConfig): void {\n const configPath = getConfigPath();\n const dir = dirname(configPath);\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const yaml = toYaml(config);\n writeFileSync(configPath, yaml);\n}\n\nexport function initConfig(): HooksConfig {\n const configPath = getConfigPath();\n\n if (existsSync(configPath)) {\n return loadConfig();\n }\n\n saveConfig(DEFAULT_CONFIG);\n return DEFAULT_CONFIG;\n}\n\nfunction parseYaml(content: string): Partial<HooksConfig> {\n const result: Record<string, unknown> = {};\n const lines = content.split('\\n');\n const stack: { indent: number; obj: Record<string, unknown> }[] = [\n { indent: -1, obj: result },\n ];\n\n for (const line of lines) {\n if (!line.trim() || line.trim().startsWith('#')) continue;\n\n const indent = line.search(/\\S/);\n const trimmed = line.trim();\n\n while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {\n stack.pop();\n }\n\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx === -1) continue;\n\n const key = trimmed.slice(0, colonIdx).trim();\n const value = trimmed.slice(colonIdx + 1).trim();\n\n const current = stack[stack.length - 1].obj;\n\n if (value === '' || value === '|') {\n current[key] = {};\n stack.push({ indent, obj: current[key] as Record<string, unknown> });\n } else if (value.startsWith('[') && value.endsWith(']')) {\n current[key] = value\n .slice(1, -1)\n .split(',')\n .map((s) => s.trim().replace(/['\"]/g, ''));\n } else if (value === 'true') {\n current[key] = true;\n } else if (value === 'false') {\n current[key] = false;\n } else if (/^\\d+$/.test(value)) {\n current[key] = parseInt(value, 10);\n } else {\n current[key] = value.replace(/['\"]/g, '');\n }\n }\n\n return result as Partial<HooksConfig>;\n}\n\nfunction toYaml(obj: unknown, indent = 0): string {\n const spaces = ' '.repeat(indent);\n let result = '';\n\n if (Array.isArray(obj)) {\n result += `[${obj.map((v) => (typeof v === 'string' ? `'${v}'` : v)).join(', ')}]\\n`;\n } else if (typeof obj === 'object' && obj !== null) {\n for (const [key, value] of Object.entries(obj)) {\n if (\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value)\n ) {\n result += `${spaces}${key}:\\n${toYaml(value, indent + 1)}`;\n } else {\n result += `${spaces}${key}: ${toYaml(value, indent)}`;\n }\n }\n } else if (typeof obj === 'string') {\n result += `${obj}\\n`;\n } else if (typeof obj === 'boolean' || typeof obj === 'number') {\n result += `${obj}\\n`;\n } else {\n result += '\\n';\n }\n\n return result;\n}\n\nfunction mergeConfig(\n defaults: HooksConfig,\n overrides: Partial<HooksConfig>\n): HooksConfig {\n return {\n ...defaults,\n ...overrides,\n daemon: { ...defaults.daemon, ...(overrides.daemon || {}) },\n file_watch: { ...defaults.file_watch, ...(overrides.file_watch || {}) },\n hooks: { ...defaults.hooks, ...(overrides.hooks || {}) },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,360 @@
1
+ import {
2
+ existsSync,
3
+ readFileSync,
4
+ writeFileSync,
5
+ unlinkSync,
6
+ watch,
7
+ appendFileSync
8
+ } from "fs";
9
+ import { join, extname, relative } from "path";
10
+ import { spawn } from "child_process";
11
+ import { loadConfig } from "./config.js";
12
+ import {
13
+ hookEmitter
14
+ } from "./events.js";
15
+ const state = {
16
+ running: false,
17
+ startTime: 0,
18
+ eventsProcessed: 0,
19
+ watchers: /* @__PURE__ */ new Map(),
20
+ pendingPrediction: false
21
+ };
22
+ let config;
23
+ let logStream = null;
24
+ function log(level, message, data) {
25
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
26
+ const line = `[${timestamp}] [${level.toUpperCase()}] ${message}${data ? " " + JSON.stringify(data) : ""}`;
27
+ if (logStream) {
28
+ logStream(line);
29
+ }
30
+ const logLevels = ["debug", "info", "warn", "error"];
31
+ const configLevel = logLevels.indexOf(config?.daemon?.log_level || "info");
32
+ const msgLevel = logLevels.indexOf(level);
33
+ if (msgLevel >= configLevel) {
34
+ if (level === "error") {
35
+ console.error(line);
36
+ } else {
37
+ console.log(line);
38
+ }
39
+ }
40
+ }
41
+ async function startDaemon(options = {}) {
42
+ config = loadConfig();
43
+ if (!config.daemon.enabled) {
44
+ log("warn", "Daemon is disabled in config");
45
+ return;
46
+ }
47
+ const pidFile = config.daemon.pid_file;
48
+ if (existsSync(pidFile)) {
49
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
50
+ try {
51
+ process.kill(pid, 0);
52
+ log("warn", "Daemon already running", { pid });
53
+ return;
54
+ } catch {
55
+ unlinkSync(pidFile);
56
+ }
57
+ }
58
+ if (!options.foreground) {
59
+ const child = spawn(
60
+ process.argv[0],
61
+ [...process.argv.slice(1), "--foreground"],
62
+ {
63
+ detached: true,
64
+ stdio: "ignore"
65
+ }
66
+ );
67
+ child.unref();
68
+ log("info", "Daemon started in background", { pid: child.pid });
69
+ return;
70
+ }
71
+ writeFileSync(pidFile, process.pid.toString());
72
+ state.running = true;
73
+ state.startTime = Date.now();
74
+ log("info", "Hook daemon starting", { pid: process.pid });
75
+ setupLogStream();
76
+ registerBuiltinHandlers();
77
+ startFileWatchers();
78
+ setupSignalHandlers();
79
+ hookEmitter.emitHook({
80
+ type: "session_start",
81
+ timestamp: Date.now(),
82
+ data: { pid: process.pid }
83
+ });
84
+ log("info", "Hook daemon ready", {
85
+ events: hookEmitter.getRegisteredEvents(),
86
+ watching: Array.from(state.watchers.keys())
87
+ });
88
+ await new Promise(() => {
89
+ });
90
+ }
91
+ function stopDaemon() {
92
+ const pidFile = config?.daemon?.pid_file || join(process.env.HOME || "/tmp", ".stackmemory", "hooks.pid");
93
+ if (!existsSync(pidFile)) {
94
+ log("info", "Daemon not running");
95
+ return;
96
+ }
97
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
98
+ try {
99
+ process.kill(pid, "SIGTERM");
100
+ log("info", "Daemon stopped", { pid });
101
+ } catch {
102
+ log("warn", "Could not stop daemon", { pid });
103
+ }
104
+ try {
105
+ unlinkSync(pidFile);
106
+ } catch {
107
+ }
108
+ }
109
+ function getDaemonStatus() {
110
+ config = loadConfig();
111
+ const pidFile = config.daemon.pid_file;
112
+ if (!existsSync(pidFile)) {
113
+ return { running: false };
114
+ }
115
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
116
+ try {
117
+ process.kill(pid, 0);
118
+ return {
119
+ running: true,
120
+ pid,
121
+ uptime: state.running ? Date.now() - state.startTime : void 0,
122
+ eventsProcessed: state.eventsProcessed
123
+ };
124
+ } catch {
125
+ return { running: false };
126
+ }
127
+ }
128
+ function setupLogStream() {
129
+ const logFile = config.daemon.log_file;
130
+ logStream = (msg) => {
131
+ try {
132
+ appendFileSync(logFile, msg + "\n");
133
+ } catch {
134
+ }
135
+ };
136
+ }
137
+ function registerBuiltinHandlers() {
138
+ hookEmitter.registerHandler("file_change", handleFileChange);
139
+ hookEmitter.registerHandler("suggestion_ready", handleSuggestionReady);
140
+ hookEmitter.registerHandler("error", handleError);
141
+ hookEmitter.on("*", () => {
142
+ state.eventsProcessed++;
143
+ });
144
+ }
145
+ async function handleFileChange(event) {
146
+ const fileEvent = event;
147
+ const hookConfig = config.hooks.file_change;
148
+ if (!hookConfig?.enabled) return;
149
+ log("debug", "File change detected", { path: fileEvent.data.path });
150
+ if (hookConfig.handler === "sweep-predict") {
151
+ await runSweepPrediction(fileEvent);
152
+ }
153
+ }
154
+ async function runSweepPrediction(event) {
155
+ const hookConfig = config.hooks.file_change;
156
+ if (!hookConfig) return;
157
+ if (state.pendingPrediction) {
158
+ log("debug", "Prediction already pending, skipping");
159
+ return;
160
+ }
161
+ if (state.lastPrediction) {
162
+ const cooldown = hookConfig.cooldown_ms || 1e4;
163
+ if (Date.now() - state.lastPrediction < cooldown) {
164
+ log("debug", "In cooldown period, skipping");
165
+ return;
166
+ }
167
+ }
168
+ state.pendingPrediction = true;
169
+ const debounce = hookConfig.debounce_ms || 2e3;
170
+ await new Promise((r) => setTimeout(r, debounce));
171
+ try {
172
+ const sweepScript = findSweepScript();
173
+ if (!sweepScript) {
174
+ log("warn", "Sweep script not found");
175
+ state.pendingPrediction = false;
176
+ return;
177
+ }
178
+ const filePath = event.data.path;
179
+ const content = event.data.content || (existsSync(filePath) ? readFileSync(filePath, "utf-8") : "");
180
+ const input = {
181
+ file_path: filePath,
182
+ current_content: content
183
+ };
184
+ const result = await runPythonScript(sweepScript, input);
185
+ if (result && result.success && result.predicted_content) {
186
+ state.lastPrediction = Date.now();
187
+ const suggestionEvent = {
188
+ type: "suggestion_ready",
189
+ timestamp: Date.now(),
190
+ data: {
191
+ suggestion: result.predicted_content,
192
+ source: "sweep",
193
+ confidence: result.confidence,
194
+ preview: result.predicted_content.split("\n").slice(0, 3).join("\n")
195
+ }
196
+ };
197
+ await hookEmitter.emitHook(suggestionEvent);
198
+ }
199
+ } catch (error) {
200
+ log("error", "Sweep prediction failed", {
201
+ error: error.message
202
+ });
203
+ } finally {
204
+ state.pendingPrediction = false;
205
+ }
206
+ }
207
+ function findSweepScript() {
208
+ const locations = [
209
+ join(process.env.HOME || "", ".stackmemory", "sweep", "sweep_predict.py"),
210
+ join(
211
+ process.cwd(),
212
+ "packages",
213
+ "sweep-addon",
214
+ "python",
215
+ "sweep_predict.py"
216
+ )
217
+ ];
218
+ for (const loc of locations) {
219
+ if (existsSync(loc)) {
220
+ return loc;
221
+ }
222
+ }
223
+ return null;
224
+ }
225
+ async function runPythonScript(scriptPath, input) {
226
+ return new Promise((resolve) => {
227
+ const proc = spawn("python3", [scriptPath], {
228
+ stdio: ["pipe", "pipe", "pipe"]
229
+ });
230
+ let stdout = "";
231
+ proc.stdout.on("data", (data) => stdout += data);
232
+ proc.stderr.on("data", () => {
233
+ });
234
+ proc.on("close", () => {
235
+ try {
236
+ resolve(JSON.parse(stdout.trim()));
237
+ } catch {
238
+ resolve({ success: false });
239
+ }
240
+ });
241
+ proc.on("error", () => resolve({ success: false }));
242
+ proc.stdin.write(JSON.stringify(input));
243
+ proc.stdin.end();
244
+ });
245
+ }
246
+ function handleSuggestionReady(event) {
247
+ const suggestionEvent = event;
248
+ const hookConfig = config.hooks.suggestion_ready;
249
+ if (!hookConfig?.enabled) return;
250
+ const output = hookConfig.output || "overlay";
251
+ switch (output) {
252
+ case "overlay":
253
+ displayOverlay(suggestionEvent.data);
254
+ break;
255
+ case "notification":
256
+ displayNotification(suggestionEvent.data);
257
+ break;
258
+ case "log":
259
+ log("info", "Suggestion ready", suggestionEvent.data);
260
+ break;
261
+ }
262
+ }
263
+ function displayOverlay(data) {
264
+ const preview = data.preview || data.suggestion.slice(0, 200);
265
+ console.log("\n" + "\u2500".repeat(50));
266
+ console.log(`[${data.source}] Suggestion:`);
267
+ console.log(preview);
268
+ if (data.suggestion.length > 200) console.log("...");
269
+ console.log("\u2500".repeat(50) + "\n");
270
+ }
271
+ function displayNotification(data) {
272
+ const title = `StackMemory - ${data.source}`;
273
+ const message = data.preview || data.suggestion.slice(0, 100);
274
+ if (process.platform === "darwin") {
275
+ spawn("osascript", [
276
+ "-e",
277
+ `display notification "${message}" with title "${title}"`
278
+ ]);
279
+ } else if (process.platform === "linux") {
280
+ spawn("notify-send", [title, message]);
281
+ }
282
+ }
283
+ function handleError(event) {
284
+ log("error", "Hook error", event.data);
285
+ }
286
+ function startFileWatchers() {
287
+ if (!config.file_watch.enabled) return;
288
+ const paths = config.file_watch.paths;
289
+ const ignore = new Set(config.file_watch.ignore);
290
+ const extensions = new Set(config.file_watch.extensions);
291
+ for (const watchPath of paths) {
292
+ const absPath = join(process.cwd(), watchPath);
293
+ if (!existsSync(absPath)) continue;
294
+ try {
295
+ const watcher = watch(
296
+ absPath,
297
+ { recursive: true },
298
+ (eventType, filename) => {
299
+ if (!filename) return;
300
+ const relPath = relative(absPath, join(absPath, filename));
301
+ const parts = relPath.split("/");
302
+ if (parts.some((p) => ignore.has(p))) return;
303
+ const ext = extname(filename);
304
+ if (!extensions.has(ext)) return;
305
+ const fullPath = join(absPath, filename);
306
+ const changeType = eventType === "rename" ? existsSync(fullPath) ? "create" : "delete" : "modify";
307
+ const fileEvent = {
308
+ type: "file_change",
309
+ timestamp: Date.now(),
310
+ data: {
311
+ path: fullPath,
312
+ changeType,
313
+ content: changeType !== "delete" && existsSync(fullPath) ? readFileSync(fullPath, "utf-8") : void 0
314
+ }
315
+ };
316
+ hookEmitter.emitHook(fileEvent);
317
+ }
318
+ );
319
+ state.watchers.set(absPath, watcher);
320
+ log("debug", "Watching directory", { path: absPath });
321
+ } catch (error) {
322
+ log("warn", "Failed to watch directory", {
323
+ path: absPath,
324
+ error: error.message
325
+ });
326
+ }
327
+ }
328
+ }
329
+ function setupSignalHandlers() {
330
+ const cleanup = () => {
331
+ log("info", "Daemon shutting down");
332
+ state.running = false;
333
+ for (const [path, watcher] of state.watchers) {
334
+ watcher.close();
335
+ log("debug", "Stopped watching", { path });
336
+ }
337
+ hookEmitter.emitHook({
338
+ type: "session_end",
339
+ timestamp: Date.now(),
340
+ data: { uptime: Date.now() - state.startTime }
341
+ });
342
+ try {
343
+ unlinkSync(config.daemon.pid_file);
344
+ } catch {
345
+ }
346
+ process.exit(0);
347
+ };
348
+ process.on("SIGTERM", cleanup);
349
+ process.on("SIGINT", cleanup);
350
+ process.on("SIGHUP", cleanup);
351
+ }
352
+ export {
353
+ config,
354
+ getDaemonStatus,
355
+ log,
356
+ startDaemon,
357
+ state,
358
+ stopDaemon
359
+ };
360
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/daemon.ts"],
4
+ "sourcesContent": ["/**\n * StackMemory Hook Daemon\n * Background process that manages hooks and events\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n unlinkSync,\n watch,\n appendFileSync,\n} from 'fs';\nimport { join, extname, relative } from 'path';\nimport { spawn } from 'child_process';\nimport { loadConfig, HooksConfig } from './config.js';\nimport {\n hookEmitter,\n HookEventData,\n FileChangeEvent,\n SuggestionReadyEvent,\n} from './events.js';\n\ninterface DaemonState {\n running: boolean;\n startTime: number;\n eventsProcessed: number;\n lastEvent?: HookEventData;\n watchers: Map<string, ReturnType<typeof watch>>;\n pendingPrediction: boolean;\n lastPrediction?: number;\n}\n\nconst state: DaemonState = {\n running: false,\n startTime: 0,\n eventsProcessed: 0,\n watchers: new Map(),\n pendingPrediction: false,\n};\n\nlet config: HooksConfig;\nlet logStream: ((msg: string) => void) | null = null;\n\nexport function log(level: string, message: string, data?: unknown): void {\n const timestamp = new Date().toISOString();\n const line = `[${timestamp}] [${level.toUpperCase()}] ${message}${data ? ' ' + JSON.stringify(data) : ''}`;\n\n if (logStream) {\n logStream(line);\n }\n\n const logLevels = ['debug', 'info', 'warn', 'error'];\n const configLevel = logLevels.indexOf(config?.daemon?.log_level || 'info');\n const msgLevel = logLevels.indexOf(level);\n\n if (msgLevel >= configLevel) {\n if (level === 'error') {\n console.error(line);\n } else {\n console.log(line);\n }\n }\n}\n\nexport async function startDaemon(\n options: { foreground?: boolean } = {}\n): Promise<void> {\n config = loadConfig();\n\n if (!config.daemon.enabled) {\n log('warn', 'Daemon is disabled in config');\n return;\n }\n\n const pidFile = config.daemon.pid_file;\n\n if (existsSync(pidFile)) {\n const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);\n try {\n process.kill(pid, 0);\n log('warn', 'Daemon already running', { pid });\n return;\n } catch {\n unlinkSync(pidFile);\n }\n }\n\n if (!options.foreground) {\n const child = spawn(\n process.argv[0],\n [...process.argv.slice(1), '--foreground'],\n {\n detached: true,\n stdio: 'ignore',\n }\n );\n child.unref();\n log('info', 'Daemon started in background', { pid: child.pid });\n return;\n }\n\n writeFileSync(pidFile, process.pid.toString());\n state.running = true;\n state.startTime = Date.now();\n\n log('info', 'Hook daemon starting', { pid: process.pid });\n\n setupLogStream();\n registerBuiltinHandlers();\n startFileWatchers();\n setupSignalHandlers();\n\n hookEmitter.emitHook({\n type: 'session_start',\n timestamp: Date.now(),\n data: { pid: process.pid },\n });\n\n log('info', 'Hook daemon ready', {\n events: hookEmitter.getRegisteredEvents(),\n watching: Array.from(state.watchers.keys()),\n });\n\n await new Promise(() => {});\n}\n\nexport function stopDaemon(): void {\n const pidFile =\n config?.daemon?.pid_file ||\n join(process.env.HOME || '/tmp', '.stackmemory', 'hooks.pid');\n\n if (!existsSync(pidFile)) {\n log('info', 'Daemon not running');\n return;\n }\n\n const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);\n\n try {\n process.kill(pid, 'SIGTERM');\n log('info', 'Daemon stopped', { pid });\n } catch {\n log('warn', 'Could not stop daemon', { pid });\n }\n\n try {\n unlinkSync(pidFile);\n } catch {\n // Ignore\n }\n}\n\nexport function getDaemonStatus(): {\n running: boolean;\n pid?: number;\n uptime?: number;\n eventsProcessed?: number;\n} {\n config = loadConfig();\n const pidFile = config.daemon.pid_file;\n\n if (!existsSync(pidFile)) {\n return { running: false };\n }\n\n const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);\n\n try {\n process.kill(pid, 0);\n return {\n running: true,\n pid,\n uptime: state.running ? Date.now() - state.startTime : undefined,\n eventsProcessed: state.eventsProcessed,\n };\n } catch {\n return { running: false };\n }\n}\n\nfunction setupLogStream(): void {\n const logFile = config.daemon.log_file;\n\n logStream = (msg: string) => {\n try {\n appendFileSync(logFile, msg + '\\n');\n } catch {\n // Ignore\n }\n };\n}\n\nfunction registerBuiltinHandlers(): void {\n hookEmitter.registerHandler('file_change', handleFileChange);\n hookEmitter.registerHandler('suggestion_ready', handleSuggestionReady);\n hookEmitter.registerHandler('error', handleError);\n\n hookEmitter.on('*', () => {\n state.eventsProcessed++;\n });\n}\n\nasync function handleFileChange(event: HookEventData): Promise<void> {\n const fileEvent = event as FileChangeEvent;\n const hookConfig = config.hooks.file_change;\n\n if (!hookConfig?.enabled) return;\n\n log('debug', 'File change detected', { path: fileEvent.data.path });\n\n if (hookConfig.handler === 'sweep-predict') {\n await runSweepPrediction(fileEvent);\n }\n}\n\nasync function runSweepPrediction(event: FileChangeEvent): Promise<void> {\n const hookConfig = config.hooks.file_change;\n if (!hookConfig) return;\n\n if (state.pendingPrediction) {\n log('debug', 'Prediction already pending, skipping');\n return;\n }\n\n if (state.lastPrediction) {\n const cooldown = hookConfig.cooldown_ms || 10000;\n if (Date.now() - state.lastPrediction < cooldown) {\n log('debug', 'In cooldown period, skipping');\n return;\n }\n }\n\n state.pendingPrediction = true;\n\n const debounce = hookConfig.debounce_ms || 2000;\n await new Promise((r) => setTimeout(r, debounce));\n\n try {\n const sweepScript = findSweepScript();\n if (!sweepScript) {\n log('warn', 'Sweep script not found');\n state.pendingPrediction = false;\n return;\n }\n\n const filePath = event.data.path;\n const content =\n event.data.content ||\n (existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '');\n\n const input = {\n file_path: filePath,\n current_content: content,\n };\n\n const result = await runPythonScript(sweepScript, input);\n\n if (result && result.success && result.predicted_content) {\n state.lastPrediction = Date.now();\n\n const suggestionEvent: SuggestionReadyEvent = {\n type: 'suggestion_ready',\n timestamp: Date.now(),\n data: {\n suggestion: result.predicted_content,\n source: 'sweep',\n confidence: result.confidence,\n preview: result.predicted_content.split('\\n').slice(0, 3).join('\\n'),\n },\n };\n\n await hookEmitter.emitHook(suggestionEvent);\n }\n } catch (error) {\n log('error', 'Sweep prediction failed', {\n error: (error as Error).message,\n });\n } finally {\n state.pendingPrediction = false;\n }\n}\n\nfunction findSweepScript(): string | null {\n const locations = [\n join(process.env.HOME || '', '.stackmemory', 'sweep', 'sweep_predict.py'),\n join(\n process.cwd(),\n 'packages',\n 'sweep-addon',\n 'python',\n 'sweep_predict.py'\n ),\n ];\n\n for (const loc of locations) {\n if (existsSync(loc)) {\n return loc;\n }\n }\n return null;\n}\n\nasync function runPythonScript(\n scriptPath: string,\n input: Record<string, unknown>\n): Promise<{\n success: boolean;\n predicted_content?: string;\n confidence?: number;\n}> {\n return new Promise((resolve) => {\n const proc = spawn('python3', [scriptPath], {\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n proc.stdout.on('data', (data) => (stdout += data));\n proc.stderr.on('data', () => {});\n\n proc.on('close', () => {\n try {\n resolve(JSON.parse(stdout.trim()));\n } catch {\n resolve({ success: false });\n }\n });\n\n proc.on('error', () => resolve({ success: false }));\n\n proc.stdin.write(JSON.stringify(input));\n proc.stdin.end();\n });\n}\n\nfunction handleSuggestionReady(event: HookEventData): void {\n const suggestionEvent = event as SuggestionReadyEvent;\n const hookConfig = config.hooks.suggestion_ready;\n\n if (!hookConfig?.enabled) return;\n\n const output = hookConfig.output || 'overlay';\n\n switch (output) {\n case 'overlay':\n displayOverlay(suggestionEvent.data);\n break;\n case 'notification':\n displayNotification(suggestionEvent.data);\n break;\n case 'log':\n log('info', 'Suggestion ready', suggestionEvent.data);\n break;\n }\n}\n\nfunction displayOverlay(data: SuggestionReadyEvent['data']): void {\n const preview = data.preview || data.suggestion.slice(0, 200);\n console.log('\\n' + '\u2500'.repeat(50));\n console.log(`[${data.source}] Suggestion:`);\n console.log(preview);\n if (data.suggestion.length > 200) console.log('...');\n console.log('\u2500'.repeat(50) + '\\n');\n}\n\nfunction displayNotification(data: SuggestionReadyEvent['data']): void {\n const title = `StackMemory - ${data.source}`;\n const message = data.preview || data.suggestion.slice(0, 100);\n\n if (process.platform === 'darwin') {\n spawn('osascript', [\n '-e',\n `display notification \"${message}\" with title \"${title}\"`,\n ]);\n } else if (process.platform === 'linux') {\n spawn('notify-send', [title, message]);\n }\n}\n\nfunction handleError(event: HookEventData): void {\n log('error', 'Hook error', event.data);\n}\n\nfunction startFileWatchers(): void {\n if (!config.file_watch.enabled) return;\n\n const paths = config.file_watch.paths;\n const ignore = new Set(config.file_watch.ignore);\n const extensions = new Set(config.file_watch.extensions);\n\n for (const watchPath of paths) {\n const absPath = join(process.cwd(), watchPath);\n if (!existsSync(absPath)) continue;\n\n try {\n const watcher = watch(\n absPath,\n { recursive: true },\n (eventType, filename) => {\n if (!filename) return;\n\n const relPath = relative(absPath, join(absPath, filename));\n const parts = relPath.split('/');\n\n if (parts.some((p) => ignore.has(p))) return;\n\n const ext = extname(filename);\n if (!extensions.has(ext)) return;\n\n const fullPath = join(absPath, filename);\n const changeType =\n eventType === 'rename'\n ? existsSync(fullPath)\n ? 'create'\n : 'delete'\n : 'modify';\n\n const fileEvent: FileChangeEvent = {\n type: 'file_change',\n timestamp: Date.now(),\n data: {\n path: fullPath,\n changeType,\n content:\n changeType !== 'delete' && existsSync(fullPath)\n ? readFileSync(fullPath, 'utf-8')\n : undefined,\n },\n };\n\n hookEmitter.emitHook(fileEvent);\n }\n );\n\n state.watchers.set(absPath, watcher);\n log('debug', 'Watching directory', { path: absPath });\n } catch (error) {\n log('warn', 'Failed to watch directory', {\n path: absPath,\n error: (error as Error).message,\n });\n }\n }\n}\n\nfunction setupSignalHandlers(): void {\n const cleanup = () => {\n log('info', 'Daemon shutting down');\n state.running = false;\n\n for (const [path, watcher] of state.watchers) {\n watcher.close();\n log('debug', 'Stopped watching', { path });\n }\n\n hookEmitter.emitHook({\n type: 'session_end',\n timestamp: Date.now(),\n data: { uptime: Date.now() - state.startTime },\n });\n\n try {\n unlinkSync(config.daemon.pid_file);\n } catch {\n // Ignore\n }\n\n process.exit(0);\n };\n\n process.on('SIGTERM', cleanup);\n process.on('SIGINT', cleanup);\n process.on('SIGHUP', cleanup);\n}\n\nexport { config, state };\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,51 @@
1
+ import { EventEmitter } from "events";
2
+ class HookEventEmitter extends EventEmitter {
3
+ handlers = /* @__PURE__ */ new Map();
4
+ registerHandler(eventType, handler) {
5
+ if (!this.handlers.has(eventType)) {
6
+ this.handlers.set(eventType, /* @__PURE__ */ new Set());
7
+ }
8
+ this.handlers.get(eventType).add(handler);
9
+ this.on(eventType, handler);
10
+ }
11
+ unregisterHandler(eventType, handler) {
12
+ const handlers = this.handlers.get(eventType);
13
+ if (handlers) {
14
+ handlers.delete(handler);
15
+ this.off(eventType, handler);
16
+ }
17
+ }
18
+ async emitHook(event) {
19
+ const handlers = this.handlers.get(event.type);
20
+ if (!handlers || handlers.size === 0) {
21
+ return;
22
+ }
23
+ const promises = [];
24
+ for (const handler of handlers) {
25
+ try {
26
+ const result = handler(event);
27
+ if (result instanceof Promise) {
28
+ promises.push(result);
29
+ }
30
+ } catch (error) {
31
+ this.emit("error", {
32
+ type: "error",
33
+ timestamp: Date.now(),
34
+ data: { error, originalEvent: event }
35
+ });
36
+ }
37
+ }
38
+ await Promise.allSettled(promises);
39
+ }
40
+ getRegisteredEvents() {
41
+ return Array.from(this.handlers.keys()).filter(
42
+ (type) => (this.handlers.get(type)?.size ?? 0) > 0
43
+ );
44
+ }
45
+ }
46
+ const hookEmitter = new HookEventEmitter();
47
+ export {
48
+ HookEventEmitter,
49
+ hookEmitter
50
+ };
51
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/events.ts"],
4
+ "sourcesContent": ["/**\n * StackMemory Hook Events\n * Event types and emitter for the hook system\n */\n\nimport { EventEmitter } from 'events';\n\nexport type HookEventType =\n | 'input_idle'\n | 'file_change'\n | 'context_switch'\n | 'session_start'\n | 'session_end'\n | 'prompt_submit'\n | 'tool_use'\n | 'suggestion_ready'\n | 'error';\n\nexport interface HookEvent {\n type: HookEventType;\n timestamp: number;\n data: Record<string, unknown>;\n}\n\nexport interface FileChangeEvent extends HookEvent {\n type: 'file_change';\n data: {\n path: string;\n changeType: 'create' | 'modify' | 'delete';\n content?: string;\n };\n}\n\nexport interface InputIdleEvent extends HookEvent {\n type: 'input_idle';\n data: {\n idleDuration: number;\n lastInput?: string;\n };\n}\n\nexport interface ContextSwitchEvent extends HookEvent {\n type: 'context_switch';\n data: {\n fromBranch?: string;\n toBranch?: string;\n fromProject?: string;\n toProject?: string;\n };\n}\n\nexport interface SuggestionReadyEvent extends HookEvent {\n type: 'suggestion_ready';\n data: {\n suggestion: string;\n source: string;\n confidence?: number;\n preview?: string;\n };\n}\n\nexport type HookEventData =\n | FileChangeEvent\n | InputIdleEvent\n | ContextSwitchEvent\n | SuggestionReadyEvent\n | HookEvent;\n\nexport type HookHandler = (event: HookEventData) => Promise<void> | void;\n\nexport class HookEventEmitter extends EventEmitter {\n private handlers: Map<HookEventType, Set<HookHandler>> = new Map();\n\n registerHandler(eventType: HookEventType, handler: HookHandler): void {\n if (!this.handlers.has(eventType)) {\n this.handlers.set(eventType, new Set());\n }\n this.handlers.get(eventType)!.add(handler);\n this.on(eventType, handler);\n }\n\n unregisterHandler(eventType: HookEventType, handler: HookHandler): void {\n const handlers = this.handlers.get(eventType);\n if (handlers) {\n handlers.delete(handler);\n this.off(eventType, handler);\n }\n }\n\n async emitHook(event: HookEventData): Promise<void> {\n const handlers = this.handlers.get(event.type);\n if (!handlers || handlers.size === 0) {\n return;\n }\n\n const promises: Promise<void>[] = [];\n for (const handler of handlers) {\n try {\n const result = handler(event);\n if (result instanceof Promise) {\n promises.push(result);\n }\n } catch (error) {\n this.emit('error', {\n type: 'error',\n timestamp: Date.now(),\n data: { error, originalEvent: event },\n });\n }\n }\n\n await Promise.allSettled(promises);\n }\n\n getRegisteredEvents(): HookEventType[] {\n return Array.from(this.handlers.keys()).filter(\n (type) => (this.handlers.get(type)?.size ?? 0) > 0\n );\n }\n}\n\nexport const hookEmitter = new HookEventEmitter();\n"],
5
+ "mappings": "AAKA,SAAS,oBAAoB;AAiEtB,MAAM,yBAAyB,aAAa;AAAA,EACzC,WAAiD,oBAAI,IAAI;AAAA,EAEjE,gBAAgB,WAA0B,SAA4B;AACpE,QAAI,CAAC,KAAK,SAAS,IAAI,SAAS,GAAG;AACjC,WAAK,SAAS,IAAI,WAAW,oBAAI,IAAI,CAAC;AAAA,IACxC;AACA,SAAK,SAAS,IAAI,SAAS,EAAG,IAAI,OAAO;AACzC,SAAK,GAAG,WAAW,OAAO;AAAA,EAC5B;AAAA,EAEA,kBAAkB,WAA0B,SAA4B;AACtE,UAAM,WAAW,KAAK,SAAS,IAAI,SAAS;AAC5C,QAAI,UAAU;AACZ,eAAS,OAAO,OAAO;AACvB,WAAK,IAAI,WAAW,OAAO;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,OAAqC;AAClD,UAAM,WAAW,KAAK,SAAS,IAAI,MAAM,IAAI;AAC7C,QAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AACpC;AAAA,IACF;AAEA,UAAM,WAA4B,CAAC;AACnC,eAAW,WAAW,UAAU;AAC9B,UAAI;AACF,cAAM,SAAS,QAAQ,KAAK;AAC5B,YAAI,kBAAkB,SAAS;AAC7B,mBAAS,KAAK,MAAM;AAAA,QACtB;AAAA,MACF,SAAS,OAAO;AACd,aAAK,KAAK,SAAS;AAAA,UACjB,MAAM;AAAA,UACN,WAAW,KAAK,IAAI;AAAA,UACpB,MAAM,EAAE,OAAO,eAAe,MAAM;AAAA,QACtC,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,QAAQ,WAAW,QAAQ;AAAA,EACnC;AAAA,EAEA,sBAAuC;AACrC,WAAO,MAAM,KAAK,KAAK,SAAS,KAAK,CAAC,EAAE;AAAA,MACtC,CAAC,UAAU,KAAK,SAAS,IAAI,IAAI,GAAG,QAAQ,KAAK;AAAA,IACnD;AAAA,EACF;AACF;AAEO,MAAM,cAAc,IAAI,iBAAiB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./events.js";
2
+ export * from "./config.js";
3
+ export * from "./daemon.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/index.ts"],
4
+ "sourcesContent": ["/**\n * StackMemory Hooks Module\n * User-configurable hook system for automation and suggestions\n */\n\nexport * from './events.js';\nexport * from './config.js';\nexport * from './daemon.js';\n"],
5
+ "mappings": "AAKA,cAAc;AACd,cAAc;AACd,cAAc;",
6
+ "names": []
7
+ }