@geminixiang/mama 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +158 -0
  3. package/dist/adapter.d.ts +38 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +2 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/adapters/slack/bot.d.ts +130 -0
  8. package/dist/adapters/slack/bot.d.ts.map +1 -0
  9. package/dist/adapters/slack/bot.js +516 -0
  10. package/dist/adapters/slack/bot.js.map +1 -0
  11. package/dist/adapters/slack/context.d.ts +11 -0
  12. package/dist/adapters/slack/context.d.ts.map +1 -0
  13. package/dist/adapters/slack/context.js +178 -0
  14. package/dist/adapters/slack/context.js.map +1 -0
  15. package/dist/adapters/slack/index.d.ts +3 -0
  16. package/dist/adapters/slack/index.d.ts.map +1 -0
  17. package/dist/adapters/slack/index.js +3 -0
  18. package/dist/adapters/slack/index.js.map +1 -0
  19. package/dist/adapters/slack/tools/attach.d.ts +12 -0
  20. package/dist/adapters/slack/tools/attach.d.ts.map +1 -0
  21. package/dist/adapters/slack/tools/attach.js +38 -0
  22. package/dist/adapters/slack/tools/attach.js.map +1 -0
  23. package/dist/agent.d.ts +26 -0
  24. package/dist/agent.d.ts.map +1 -0
  25. package/dist/agent.js +763 -0
  26. package/dist/agent.js.map +1 -0
  27. package/dist/config.d.ts +10 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +54 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/context.d.ts +34 -0
  32. package/dist/context.d.ts.map +1 -0
  33. package/dist/context.js +110 -0
  34. package/dist/context.js.map +1 -0
  35. package/dist/download.d.ts +2 -0
  36. package/dist/download.d.ts.map +1 -0
  37. package/dist/download.js +89 -0
  38. package/dist/download.js.map +1 -0
  39. package/dist/events.d.ts +57 -0
  40. package/dist/events.d.ts.map +1 -0
  41. package/dist/events.js +310 -0
  42. package/dist/events.js.map +1 -0
  43. package/dist/log.d.ts +39 -0
  44. package/dist/log.d.ts.map +1 -0
  45. package/dist/log.js +222 -0
  46. package/dist/log.js.map +1 -0
  47. package/dist/main.d.ts +3 -0
  48. package/dist/main.d.ts.map +1 -0
  49. package/dist/main.js +247 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/sandbox.d.ts +34 -0
  52. package/dist/sandbox.d.ts.map +1 -0
  53. package/dist/sandbox.js +183 -0
  54. package/dist/sandbox.js.map +1 -0
  55. package/dist/store.d.ts +60 -0
  56. package/dist/store.d.ts.map +1 -0
  57. package/dist/store.js +180 -0
  58. package/dist/store.js.map +1 -0
  59. package/dist/tools/bash.d.ts +10 -0
  60. package/dist/tools/bash.d.ts.map +1 -0
  61. package/dist/tools/bash.js +78 -0
  62. package/dist/tools/bash.js.map +1 -0
  63. package/dist/tools/edit.d.ts +11 -0
  64. package/dist/tools/edit.d.ts.map +1 -0
  65. package/dist/tools/edit.js +131 -0
  66. package/dist/tools/edit.js.map +1 -0
  67. package/dist/tools/index.d.ts +7 -0
  68. package/dist/tools/index.d.ts.map +1 -0
  69. package/dist/tools/index.js +19 -0
  70. package/dist/tools/index.js.map +1 -0
  71. package/dist/tools/read.d.ts +11 -0
  72. package/dist/tools/read.d.ts.map +1 -0
  73. package/dist/tools/read.js +134 -0
  74. package/dist/tools/read.js.map +1 -0
  75. package/dist/tools/truncate.d.ts +57 -0
  76. package/dist/tools/truncate.d.ts.map +1 -0
  77. package/dist/tools/truncate.js +184 -0
  78. package/dist/tools/truncate.js.map +1 -0
  79. package/dist/tools/write.d.ts +10 -0
  80. package/dist/tools/write.d.ts.map +1 -0
  81. package/dist/tools/write.js +33 -0
  82. package/dist/tools/write.js.map +1 -0
  83. package/package.json +57 -0
package/dist/log.js ADDED
@@ -0,0 +1,222 @@
1
+ import chalk from "chalk";
2
+ function timestamp() {
3
+ const now = new Date();
4
+ const hh = String(now.getHours()).padStart(2, "0");
5
+ const mm = String(now.getMinutes()).padStart(2, "0");
6
+ const ss = String(now.getSeconds()).padStart(2, "0");
7
+ return `[${hh}:${mm}:${ss}]`;
8
+ }
9
+ function formatContext(ctx) {
10
+ // DMs: [DM:username]
11
+ // Channels: [#channel-name:username] or [C16HET4EQ:username] if no name
12
+ if (ctx.channelId.startsWith("D")) {
13
+ return `[DM:${ctx.userName || ctx.channelId}]`;
14
+ }
15
+ const channel = ctx.channelName || ctx.channelId;
16
+ const user = ctx.userName || "unknown";
17
+ return `[${channel.startsWith("#") ? channel : `#${channel}`}:${user}]`;
18
+ }
19
+ function truncate(text, maxLen) {
20
+ if (text.length <= maxLen)
21
+ return text;
22
+ return `${text.substring(0, maxLen)}\n(truncated at ${maxLen} chars)`;
23
+ }
24
+ function formatToolArgs(args) {
25
+ const lines = [];
26
+ for (const [key, value] of Object.entries(args)) {
27
+ // Skip the label - it's already shown in the tool name
28
+ if (key === "label")
29
+ continue;
30
+ // For read tool, format path with offset/limit
31
+ if (key === "path" && typeof value === "string") {
32
+ const offset = args.offset;
33
+ const limit = args.limit;
34
+ if (offset !== undefined && limit !== undefined) {
35
+ lines.push(`${value}:${offset}-${offset + limit}`);
36
+ }
37
+ else {
38
+ lines.push(value);
39
+ }
40
+ continue;
41
+ }
42
+ // Skip offset/limit since we already handled them
43
+ if (key === "offset" || key === "limit")
44
+ continue;
45
+ // For other values, format them
46
+ if (typeof value === "string") {
47
+ // Multi-line strings get indented
48
+ if (value.includes("\n")) {
49
+ lines.push(value);
50
+ }
51
+ else {
52
+ lines.push(value);
53
+ }
54
+ }
55
+ else {
56
+ lines.push(JSON.stringify(value));
57
+ }
58
+ }
59
+ return lines.join("\n");
60
+ }
61
+ // User messages
62
+ export function logUserMessage(ctx, text) {
63
+ console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));
64
+ }
65
+ // Tool execution
66
+ export function logToolStart(ctx, toolName, label, args) {
67
+ const formattedArgs = formatToolArgs(args);
68
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));
69
+ if (formattedArgs) {
70
+ // Indent the args
71
+ const indented = formattedArgs
72
+ .split("\n")
73
+ .map((line) => ` ${line}`)
74
+ .join("\n");
75
+ console.log(chalk.dim(indented));
76
+ }
77
+ }
78
+ export function logToolSuccess(ctx, toolName, durationMs, result) {
79
+ const duration = (durationMs / 1000).toFixed(1);
80
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));
81
+ const truncated = truncate(result, 1000);
82
+ if (truncated) {
83
+ const indented = truncated
84
+ .split("\n")
85
+ .map((line) => ` ${line}`)
86
+ .join("\n");
87
+ console.log(chalk.dim(indented));
88
+ }
89
+ }
90
+ export function logToolError(ctx, toolName, durationMs, error) {
91
+ const duration = (durationMs / 1000).toFixed(1);
92
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));
93
+ const truncated = truncate(error, 1000);
94
+ const indented = truncated
95
+ .split("\n")
96
+ .map((line) => ` ${line}`)
97
+ .join("\n");
98
+ console.log(chalk.dim(indented));
99
+ }
100
+ // Response streaming
101
+ export function logResponseStart(ctx) {
102
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));
103
+ }
104
+ export function logThinking(ctx, thinking) {
105
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));
106
+ const truncated = truncate(thinking, 1000);
107
+ const indented = truncated
108
+ .split("\n")
109
+ .map((line) => ` ${line}`)
110
+ .join("\n");
111
+ console.log(chalk.dim(indented));
112
+ }
113
+ export function logResponse(ctx, text) {
114
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));
115
+ const truncated = truncate(text, 1000);
116
+ const indented = truncated
117
+ .split("\n")
118
+ .map((line) => ` ${line}`)
119
+ .join("\n");
120
+ console.log(chalk.dim(indented));
121
+ }
122
+ // Attachments
123
+ export function logDownloadStart(ctx, filename, localPath) {
124
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));
125
+ console.log(chalk.dim(` ${filename} → ${localPath}`));
126
+ }
127
+ export function logDownloadSuccess(ctx, sizeKB) {
128
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));
129
+ }
130
+ export function logDownloadError(ctx, filename, error) {
131
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));
132
+ console.log(chalk.dim(` ${filename}: ${error}`));
133
+ }
134
+ // Control
135
+ export function logStopRequest(ctx) {
136
+ console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));
137
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));
138
+ }
139
+ // System
140
+ export function logInfo(message) {
141
+ console.log(chalk.blue(`${timestamp()} [system] ${message}`));
142
+ }
143
+ export function logWarning(message, details) {
144
+ console.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));
145
+ if (details) {
146
+ const indented = details
147
+ .split("\n")
148
+ .map((line) => ` ${line}`)
149
+ .join("\n");
150
+ console.log(chalk.dim(indented));
151
+ }
152
+ }
153
+ export function logAgentError(ctx, error) {
154
+ const context = ctx === "system" ? "[system]" : formatContext(ctx);
155
+ console.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));
156
+ const indented = error
157
+ .split("\n")
158
+ .map((line) => ` ${line}`)
159
+ .join("\n");
160
+ console.log(chalk.dim(indented));
161
+ }
162
+ // Usage summary
163
+ export function logUsageSummary(ctx, usage, contextTokens, contextWindow) {
164
+ const formatTokens = (count) => {
165
+ if (count < 1000)
166
+ return count.toString();
167
+ if (count < 10000)
168
+ return `${(count / 1000).toFixed(1)}k`;
169
+ if (count < 1000000)
170
+ return `${Math.round(count / 1000)}k`;
171
+ return `${(count / 1000000).toFixed(1)}M`;
172
+ };
173
+ const lines = [];
174
+ lines.push("*Usage Summary*");
175
+ lines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);
176
+ if (usage.cacheRead > 0 || usage.cacheWrite > 0) {
177
+ lines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);
178
+ }
179
+ if (contextTokens && contextWindow) {
180
+ const contextPercent = ((contextTokens / contextWindow) * 100).toFixed(1);
181
+ lines.push(`Context: ${formatTokens(contextTokens)} / ${formatTokens(contextWindow)} (${contextPercent}%)`);
182
+ }
183
+ lines.push(`Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +
184
+ (usage.cacheRead > 0 || usage.cacheWrite > 0
185
+ ? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`
186
+ : ""));
187
+ lines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);
188
+ const summary = lines.join("\n");
189
+ // Log to console
190
+ console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));
191
+ console.log(chalk.dim(` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +
192
+ (usage.cacheRead > 0 || usage.cacheWrite > 0
193
+ ? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`
194
+ : "") +
195
+ ` = $${usage.cost.total.toFixed(4)}`));
196
+ return summary;
197
+ }
198
+ // Startup (no context needed)
199
+ export function logStartup(workingDir, sandbox) {
200
+ console.log("Starting mama...");
201
+ console.log(` Working directory: ${workingDir}`);
202
+ console.log(` Sandbox: ${sandbox}`);
203
+ }
204
+ export function logConnected() {
205
+ console.log("⚡️ Mama connected and listening!");
206
+ console.log("");
207
+ }
208
+ export function logDisconnected() {
209
+ console.log("Mama disconnected.");
210
+ }
211
+ // Backfill
212
+ export function logBackfillStart(channelCount) {
213
+ console.log(chalk.blue(`${timestamp()} [system] Backfilling ${channelCount} channels...`));
214
+ }
215
+ export function logBackfillChannel(channelName, messageCount) {
216
+ console.log(chalk.blue(`${timestamp()} [system] #${channelName}: ${messageCount} messages`));
217
+ }
218
+ export function logBackfillComplete(totalMessages, durationMs) {
219
+ const duration = (durationMs / 1000).toFixed(1);
220
+ console.log(chalk.blue(`${timestamp()} [system] Backfill complete: ${totalMessages} messages in ${duration}s`));
221
+ }
222
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\n\nexport interface LogContext {\n\tchannelId: string;\n\tuserName?: string;\n\tchannelName?: string; // For display like #dev-team vs C16HET4EQ\n}\n\nfunction timestamp(): string {\n\tconst now = new Date();\n\tconst hh = String(now.getHours()).padStart(2, \"0\");\n\tconst mm = String(now.getMinutes()).padStart(2, \"0\");\n\tconst ss = String(now.getSeconds()).padStart(2, \"0\");\n\treturn `[${hh}:${mm}:${ss}]`;\n}\n\nfunction formatContext(ctx: LogContext): string {\n\t// DMs: [DM:username]\n\t// Channels: [#channel-name:username] or [C16HET4EQ:username] if no name\n\tif (ctx.channelId.startsWith(\"D\")) {\n\t\treturn `[DM:${ctx.userName || ctx.channelId}]`;\n\t}\n\tconst channel = ctx.channelName || ctx.channelId;\n\tconst user = ctx.userName || \"unknown\";\n\treturn `[${channel.startsWith(\"#\") ? channel : `#${channel}`}:${user}]`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.substring(0, maxLen)}\\n(truncated at ${maxLen} chars)`;\n}\n\nfunction formatToolArgs(args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown in the tool name\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\t// Multi-line strings get indented\n\t\t\tif (value.includes(\"\\n\")) {\n\t\t\t\tlines.push(value);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\n// User messages\nexport function logUserMessage(ctx: LogContext, text: string): void {\n\tconsole.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));\n}\n\n// Tool execution\nexport function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void {\n\tconst formattedArgs = formatToolArgs(args);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));\n\tif (formattedArgs) {\n\t\t// Indent the args\n\t\tconst indented = formattedArgs\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));\n\n\tconst truncated = truncate(result, 1000);\n\tif (truncated) {\n\t\tconst indented = truncated\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));\n\n\tconst truncated = truncate(error, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Response streaming\nexport function logResponseStart(ctx: LogContext): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));\n}\n\nexport function logThinking(ctx: LogContext, thinking: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));\n\tconst truncated = truncate(thinking, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\nexport function logResponse(ctx: LogContext, text: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));\n\tconst truncated = truncate(text, 1000);\n\tconst indented = truncated\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Attachments\nexport function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));\n\tconsole.log(chalk.dim(` ${filename} → ${localPath}`));\n}\n\nexport function logDownloadSuccess(ctx: LogContext, sizeKB: number): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));\n}\n\nexport function logDownloadError(ctx: LogContext, filename: string, error: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));\n\tconsole.log(chalk.dim(` ${filename}: ${error}`));\n}\n\n// Control\nexport function logStopRequest(ctx: LogContext): void {\n\tconsole.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));\n}\n\n// System\nexport function logInfo(message: string): void {\n\tconsole.log(chalk.blue(`${timestamp()} [system] ${message}`));\n}\n\nexport function logWarning(message: string, details?: string): void {\n\tconsole.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));\n\tif (details) {\n\t\tconst indented = details\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => ` ${line}`)\n\t\t\t.join(\"\\n\");\n\t\tconsole.log(chalk.dim(indented));\n\t}\n}\n\nexport function logAgentError(ctx: LogContext | \"system\", error: string): void {\n\tconst context = ctx === \"system\" ? \"[system]\" : formatContext(ctx);\n\tconsole.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));\n\tconst indented = error\n\t\t.split(\"\\n\")\n\t\t.map((line) => ` ${line}`)\n\t\t.join(\"\\n\");\n\tconsole.log(chalk.dim(indented));\n}\n\n// Usage summary\nexport function logUsageSummary(\n\tctx: LogContext,\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };\n\t},\n\tcontextTokens?: number,\n\tcontextWindow?: number,\n): string {\n\tconst formatTokens = (count: number): string => {\n\t\tif (count < 1000) return count.toString();\n\t\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\t\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\t\treturn `${(count / 1000000).toFixed(1)}M`;\n\t};\n\n\tconst lines: string[] = [];\n\tlines.push(\"*Usage Summary*\");\n\tlines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);\n\tif (usage.cacheRead > 0 || usage.cacheWrite > 0) {\n\t\tlines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);\n\t}\n\tif (contextTokens && contextWindow) {\n\t\tconst contextPercent = ((contextTokens / contextWindow) * 100).toFixed(1);\n\t\tlines.push(`Context: ${formatTokens(contextTokens)} / ${formatTokens(contextWindow)} (${contextPercent}%)`);\n\t}\n\tlines.push(\n\t\t`Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +\n\t\t\t(usage.cacheRead > 0 || usage.cacheWrite > 0\n\t\t\t\t? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`\n\t\t\t\t: \"\"),\n\t);\n\tlines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);\n\n\tconst summary = lines.join(\"\\n\");\n\n\t// Log to console\n\tconsole.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));\n\tconsole.log(\n\t\tchalk.dim(\n\t\t\t` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +\n\t\t\t\t(usage.cacheRead > 0 || usage.cacheWrite > 0\n\t\t\t\t\t? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`\n\t\t\t\t\t: \"\") +\n\t\t\t\t` = $${usage.cost.total.toFixed(4)}`,\n\t\t),\n\t);\n\n\treturn summary;\n}\n\n// Startup (no context needed)\nexport function logStartup(workingDir: string, sandbox: string): void {\n\tconsole.log(\"Starting mama...\");\n\tconsole.log(` Working directory: ${workingDir}`);\n\tconsole.log(` Sandbox: ${sandbox}`);\n}\n\nexport function logConnected(): void {\n\tconsole.log(\"⚡️ Mama connected and listening!\");\n\tconsole.log(\"\");\n}\n\nexport function logDisconnected(): void {\n\tconsole.log(\"Mama disconnected.\");\n}\n\n// Backfill\nexport function logBackfillStart(channelCount: number): void {\n\tconsole.log(chalk.blue(`${timestamp()} [system] Backfilling ${channelCount} channels...`));\n}\n\nexport function logBackfillChannel(channelName: string, messageCount: number): void {\n\tconsole.log(chalk.blue(`${timestamp()} [system] #${channelName}: ${messageCount} messages`));\n}\n\nexport function logBackfillComplete(totalMessages: number, durationMs: number): void {\n\tconst duration = (durationMs / 1000).toFixed(1);\n\tconsole.log(chalk.blue(`${timestamp()} [system] Backfill complete: ${totalMessages} messages in ${duration}s`));\n}\n"]}
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport {\n\tcreateSlackAdapters,\n\ttype MomHandler,\n\ttype SlackBot,\n\tSlackBot as SlackBotClass,\n\ttype SlackEvent,\n} from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\n\ninterface ParsedArgs {\n\tworkingDir?: string;\n\tsandbox: SandboxConfig;\n\tdownloadChannel?: string;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\tlet downloadChannelId: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg.startsWith(\"--download=\")) {\n\t\t\tdownloadChannelId = arg.slice(\"--download=\".length);\n\t\t} else if (arg === \"--download\") {\n\t\t\tdownloadChannelId = args[++i];\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t}\n\t}\n\n\treturn {\n\t\tworkingDir: workingDir ? resolve(workingDir) : undefined,\n\t\tsandbox,\n\t\tdownloadChannel: downloadChannelId,\n\t};\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --download mode\nif (parsedArgs.downloadChannel) {\n\tif (!MOM_SLACK_BOT_TOKEN) {\n\t\tconsole.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n\t\tprocess.exit(1);\n\t}\n\tawait downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n\tprocess.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n\tconsole.error(\"Usage: mama [--sandbox=host|docker:<name>] <working-directory>\");\n\tconsole.error(\" mama --download <channel-id>\");\n\tprocess.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN) {\n\tconsole.error(\"Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN\");\n\tprocess.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstopRequested: boolean;\n\tstopMessageTs?: string;\n\tlastAccessedAt: number;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n\nasync function getState(channelId: string, sessionKey?: string): Promise<ChannelState> {\n\tconst key = sessionKey ?? channelId;\n\tlet state = channelStates.get(key);\n\tif (!state) {\n\t\tconst channelDir = join(workingDir, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n\t\t\tstopRequested: false,\n\t\t\tlastAccessedAt: Date.now(),\n\t\t};\n\t\tchannelStates.set(key, state);\n\t} else {\n\t\tstate.lastAccessedAt = Date.now();\n\t}\n\treturn state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n *\n * Eviction rules:\n * - Never evict sessions that are currently running\n * - Evict sessions idle for more than IDLE_TIMEOUT_MS\n * - If still over MAX_SESSIONS, evict oldest idle sessions first\n */\nfunction evictIdleSessions(): void {\n\tconst now = Date.now();\n\n\t// First pass: evict sessions that are idle and past the timeout\n\tfor (const [key, state] of channelStates) {\n\t\tif (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n\t\t\tchannelStates.delete(key);\n\t\t}\n\t}\n\n\t// Second pass: if still over capacity, evict oldest idle sessions\n\tif (channelStates.size > MAX_SESSIONS) {\n\t\t// Collect all non-running sessions with their last access time\n\t\tconst idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n\t\tfor (const [key, state] of channelStates) {\n\t\t\tif (!state.running) {\n\t\t\t\tidleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n\t\t\t}\n\t\t}\n\n\t\t// Sort oldest first\n\t\tidleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n\t\t// Evict until under capacity\n\t\tconst toEvict = channelStates.size - MAX_SESSIONS;\n\t\tfor (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n\t\t\tchannelStates.delete(idleSessions[i].key);\n\t\t}\n\t}\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: MomHandler = {\n\tisRunning(sessionKey: string): boolean {\n\t\tconst state = channelStates.get(sessionKey);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(sessionKey: string, channelId: string, slack: SlackBot): Promise<void> {\n\t\tconst state = channelStates.get(sessionKey);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tconst ts = await slack.postMessage(channelId, \"_Stopping..._\");\n\t\t\tstate.stopMessageTs = ts; // Save for updating later\n\t\t} else {\n\t\t\tawait slack.postMessage(channelId, \"_Nothing running_\");\n\t\t}\n\t},\n\n\tasync handleEvent(event: SlackEvent, slack: SlackBot, isEvent?: boolean): Promise<void> {\n\t\t// Don't accept new events during shutdown\n\t\tif (isShuttingDown) {\n\t\t\tlog.logInfo(`[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst sessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;\n\t\tconst state = await getState(event.channel, sessionKey);\n\n\t\t// Start run\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tlog.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n\t\t// Wrap in-flight run tracking\n\t\tconst runPromise = (async () => {\n\t\t\ttry {\n\t\t\t\t// Create platform-agnostic adapter objects\n\t\t\t\tconst { message, responseCtx, platform } = createSlackAdapters(event, slack, isEvent);\n\n\t\t\t\t// Run the agent\n\t\t\t\tawait responseCtx.setTyping(true);\n\t\t\t\tawait responseCtx.setWorking(true);\n\t\t\t\tconst result = await state.runner.run(message, responseCtx, platform);\n\t\t\t\tawait responseCtx.setWorking(false);\n\n\t\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\t\tif (state.stopMessageTs) {\n\t\t\t\t\t\tawait slack.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n\t\t\t\t\t\tstate.stopMessageTs = undefined;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait slack.postMessage(event.channel, \"_Stopped_\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tlog.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t\t} finally {\n\t\t\t\tstate.running = false;\n\t\t\t\tstate.lastAccessedAt = Date.now();\n\t\t\t\tevictIdleSessions();\n\t\t\t}\n\t\t})();\n\n\t\tinFlightRuns.add(runPromise);\n\t\ttry {\n\t\t\tawait runPromise;\n\t\t} finally {\n\t\t\tinFlightRuns.delete(runPromise);\n\t\t}\n\t},\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\n// Shared store for attachment downloads (also used per-channel in getState)\nconst sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n\nconst bot = new SlackBotClass(handler, {\n\tappToken: MOM_SLACK_APP_TOKEN,\n\tbotToken: MOM_SLACK_BOT_TOKEN,\n\tworkingDir,\n\tstore: sharedStore,\n});\n\n// Start events watcher\nconst eventsWatcher = createEventsWatcher(workingDir, bot);\neventsWatcher.start();\n\n// Handle shutdown\nprocess.on(\"SIGINT\", async () => {\n\tif (isShuttingDown) return; // Prevent duplicate signals\n\tisShuttingDown = true;\n\tlog.logInfo(\"Shutting down gracefully...\");\n\n\t// Wait for in-flight runs (max 30 seconds)\n\tconst timeout = Date.now() + 30000;\n\twhile (inFlightRuns.size > 0 && Date.now() < timeout) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t}\n\n\tif (inFlightRuns.size > 0) {\n\t\tlog.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n\t}\n\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", async () => {\n\tif (isShuttingDown) return; // Prevent duplicate signals\n\tisShuttingDown = true;\n\tlog.logInfo(\"Shutting down gracefully...\");\n\n\t// Wait for in-flight runs (max 30 seconds)\n\tconst timeout = Date.now() + 30000;\n\twhile (inFlightRuns.size > 0 && Date.now() < timeout) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t}\n\n\tif (inFlightRuns.size > 0) {\n\t\tlog.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n\t}\n\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nbot.start();\n"]}
package/dist/main.js ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ import { join, resolve } from "path";
3
+ import { createSlackAdapters, SlackBot as SlackBotClass, } from "./adapters/slack/index.js";
4
+ import { createRunner } from "./agent.js";
5
+ import { downloadChannel } from "./download.js";
6
+ import { createEventsWatcher } from "./events.js";
7
+ import * as log from "./log.js";
8
+ import { parseSandboxArg, validateSandbox } from "./sandbox.js";
9
+ import { ChannelStore } from "./store.js";
10
+ // ============================================================================
11
+ // Config
12
+ // ============================================================================
13
+ const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
14
+ const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
15
+ function parseArgs() {
16
+ const args = process.argv.slice(2);
17
+ let sandbox = { type: "host" };
18
+ let workingDir;
19
+ let downloadChannelId;
20
+ for (let i = 0; i < args.length; i++) {
21
+ const arg = args[i];
22
+ if (arg.startsWith("--sandbox=")) {
23
+ sandbox = parseSandboxArg(arg.slice("--sandbox=".length));
24
+ }
25
+ else if (arg === "--sandbox") {
26
+ sandbox = parseSandboxArg(args[++i] || "");
27
+ }
28
+ else if (arg.startsWith("--download=")) {
29
+ downloadChannelId = arg.slice("--download=".length);
30
+ }
31
+ else if (arg === "--download") {
32
+ downloadChannelId = args[++i];
33
+ }
34
+ else if (!arg.startsWith("-")) {
35
+ workingDir = arg;
36
+ }
37
+ }
38
+ return {
39
+ workingDir: workingDir ? resolve(workingDir) : undefined,
40
+ sandbox,
41
+ downloadChannel: downloadChannelId,
42
+ };
43
+ }
44
+ const parsedArgs = parseArgs();
45
+ // Handle --download mode
46
+ if (parsedArgs.downloadChannel) {
47
+ if (!MOM_SLACK_BOT_TOKEN) {
48
+ console.error("Missing env: MOM_SLACK_BOT_TOKEN");
49
+ process.exit(1);
50
+ }
51
+ await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);
52
+ process.exit(0);
53
+ }
54
+ // Normal bot mode - require working dir
55
+ if (!parsedArgs.workingDir) {
56
+ console.error("Usage: mama [--sandbox=host|docker:<name>] <working-directory>");
57
+ console.error(" mama --download <channel-id>");
58
+ process.exit(1);
59
+ }
60
+ const { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };
61
+ if (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN) {
62
+ console.error("Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN");
63
+ process.exit(1);
64
+ }
65
+ await validateSandbox(sandbox);
66
+ const channelStates = new Map();
67
+ /** Track in-flight runs for graceful shutdown */
68
+ const inFlightRuns = new Set();
69
+ /** Flag to stop accepting new events during shutdown */
70
+ let isShuttingDown = false;
71
+ /** Maximum number of cached sessions */
72
+ const MAX_SESSIONS = 500;
73
+ /** Idle timeout before a non-running session can be evicted (1 hour) */
74
+ const IDLE_TIMEOUT_MS = 3600000;
75
+ async function getState(channelId, sessionKey) {
76
+ const key = sessionKey ?? channelId;
77
+ let state = channelStates.get(key);
78
+ if (!state) {
79
+ const channelDir = join(workingDir, channelId);
80
+ state = {
81
+ running: false,
82
+ runner: await createRunner(sandbox, key, channelId, channelDir, workingDir),
83
+ stopRequested: false,
84
+ lastAccessedAt: Date.now(),
85
+ };
86
+ channelStates.set(key, state);
87
+ }
88
+ else {
89
+ state.lastAccessedAt = Date.now();
90
+ }
91
+ return state;
92
+ }
93
+ /**
94
+ * Evict idle sessions from channelStates to bound memory usage.
95
+ * Called after each handleEvent completes.
96
+ *
97
+ * Eviction rules:
98
+ * - Never evict sessions that are currently running
99
+ * - Evict sessions idle for more than IDLE_TIMEOUT_MS
100
+ * - If still over MAX_SESSIONS, evict oldest idle sessions first
101
+ */
102
+ function evictIdleSessions() {
103
+ const now = Date.now();
104
+ // First pass: evict sessions that are idle and past the timeout
105
+ for (const [key, state] of channelStates) {
106
+ if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
107
+ channelStates.delete(key);
108
+ }
109
+ }
110
+ // Second pass: if still over capacity, evict oldest idle sessions
111
+ if (channelStates.size > MAX_SESSIONS) {
112
+ // Collect all non-running sessions with their last access time
113
+ const idleSessions = [];
114
+ for (const [key, state] of channelStates) {
115
+ if (!state.running) {
116
+ idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });
117
+ }
118
+ }
119
+ // Sort oldest first
120
+ idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
121
+ // Evict until under capacity
122
+ const toEvict = channelStates.size - MAX_SESSIONS;
123
+ for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
124
+ channelStates.delete(idleSessions[i].key);
125
+ }
126
+ }
127
+ }
128
+ // ============================================================================
129
+ // Handler
130
+ // ============================================================================
131
+ const handler = {
132
+ isRunning(sessionKey) {
133
+ const state = channelStates.get(sessionKey);
134
+ return state?.running ?? false;
135
+ },
136
+ async handleStop(sessionKey, channelId, slack) {
137
+ const state = channelStates.get(sessionKey);
138
+ if (state?.running) {
139
+ state.stopRequested = true;
140
+ state.runner.abort();
141
+ const ts = await slack.postMessage(channelId, "_Stopping..._");
142
+ state.stopMessageTs = ts; // Save for updating later
143
+ }
144
+ else {
145
+ await slack.postMessage(channelId, "_Nothing running_");
146
+ }
147
+ },
148
+ async handleEvent(event, slack, isEvent) {
149
+ // Don't accept new events during shutdown
150
+ if (isShuttingDown) {
151
+ log.logInfo(`[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
152
+ return;
153
+ }
154
+ const sessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;
155
+ const state = await getState(event.channel, sessionKey);
156
+ // Start run
157
+ state.running = true;
158
+ state.stopRequested = false;
159
+ log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);
160
+ // Wrap in-flight run tracking
161
+ const runPromise = (async () => {
162
+ try {
163
+ // Create platform-agnostic adapter objects
164
+ const { message, responseCtx, platform } = createSlackAdapters(event, slack, isEvent);
165
+ // Run the agent
166
+ await responseCtx.setTyping(true);
167
+ await responseCtx.setWorking(true);
168
+ const result = await state.runner.run(message, responseCtx, platform);
169
+ await responseCtx.setWorking(false);
170
+ if (result.stopReason === "aborted" && state.stopRequested) {
171
+ if (state.stopMessageTs) {
172
+ await slack.updateMessage(event.channel, state.stopMessageTs, "_Stopped_");
173
+ state.stopMessageTs = undefined;
174
+ }
175
+ else {
176
+ await slack.postMessage(event.channel, "_Stopped_");
177
+ }
178
+ }
179
+ }
180
+ catch (err) {
181
+ log.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));
182
+ }
183
+ finally {
184
+ state.running = false;
185
+ state.lastAccessedAt = Date.now();
186
+ evictIdleSessions();
187
+ }
188
+ })();
189
+ inFlightRuns.add(runPromise);
190
+ try {
191
+ await runPromise;
192
+ }
193
+ finally {
194
+ inFlightRuns.delete(runPromise);
195
+ }
196
+ },
197
+ };
198
+ // ============================================================================
199
+ // Start
200
+ // ============================================================================
201
+ log.logStartup(workingDir, sandbox.type === "host" ? "host" : `docker:${sandbox.container}`);
202
+ // Shared store for attachment downloads (also used per-channel in getState)
203
+ const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN });
204
+ const bot = new SlackBotClass(handler, {
205
+ appToken: MOM_SLACK_APP_TOKEN,
206
+ botToken: MOM_SLACK_BOT_TOKEN,
207
+ workingDir,
208
+ store: sharedStore,
209
+ });
210
+ // Start events watcher
211
+ const eventsWatcher = createEventsWatcher(workingDir, bot);
212
+ eventsWatcher.start();
213
+ // Handle shutdown
214
+ process.on("SIGINT", async () => {
215
+ if (isShuttingDown)
216
+ return; // Prevent duplicate signals
217
+ isShuttingDown = true;
218
+ log.logInfo("Shutting down gracefully...");
219
+ // Wait for in-flight runs (max 30 seconds)
220
+ const timeout = Date.now() + 30000;
221
+ while (inFlightRuns.size > 0 && Date.now() < timeout) {
222
+ await new Promise((resolve) => setTimeout(resolve, 500));
223
+ }
224
+ if (inFlightRuns.size > 0) {
225
+ log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);
226
+ }
227
+ eventsWatcher.stop();
228
+ process.exit(0);
229
+ });
230
+ process.on("SIGTERM", async () => {
231
+ if (isShuttingDown)
232
+ return; // Prevent duplicate signals
233
+ isShuttingDown = true;
234
+ log.logInfo("Shutting down gracefully...");
235
+ // Wait for in-flight runs (max 30 seconds)
236
+ const timeout = Date.now() + 30000;
237
+ while (inFlightRuns.size > 0 && Date.now() < timeout) {
238
+ await new Promise((resolve) => setTimeout(resolve, 500));
239
+ }
240
+ if (inFlightRuns.size > 0) {
241
+ log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);
242
+ }
243
+ eventsWatcher.stop();
244
+ process.exit(0);
245
+ });
246
+ bot.start();
247
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { join, resolve } from \"path\";\nimport {\n\tcreateSlackAdapters,\n\ttype MomHandler,\n\ttype SlackBot,\n\tSlackBot as SlackBotClass,\n\ttype SlackEvent,\n} from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\n\ninterface ParsedArgs {\n\tworkingDir?: string;\n\tsandbox: SandboxConfig;\n\tdownloadChannel?: string;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\tlet workingDir: string | undefined;\n\tlet downloadChannelId: string | undefined;\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg.startsWith(\"--download=\")) {\n\t\t\tdownloadChannelId = arg.slice(\"--download=\".length);\n\t\t} else if (arg === \"--download\") {\n\t\t\tdownloadChannelId = args[++i];\n\t\t} else if (!arg.startsWith(\"-\")) {\n\t\t\tworkingDir = arg;\n\t\t}\n\t}\n\n\treturn {\n\t\tworkingDir: workingDir ? resolve(workingDir) : undefined,\n\t\tsandbox,\n\t\tdownloadChannel: downloadChannelId,\n\t};\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --download mode\nif (parsedArgs.downloadChannel) {\n\tif (!MOM_SLACK_BOT_TOKEN) {\n\t\tconsole.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n\t\tprocess.exit(1);\n\t}\n\tawait downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n\tprocess.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n\tconsole.error(\"Usage: mama [--sandbox=host|docker:<name>] <working-directory>\");\n\tconsole.error(\" mama --download <channel-id>\");\n\tprocess.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\nif (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN) {\n\tconsole.error(\"Missing env: MOM_SLACK_APP_TOKEN, MOM_SLACK_BOT_TOKEN\");\n\tprocess.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstopRequested: boolean;\n\tstopMessageTs?: string;\n\tlastAccessedAt: number;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n\nasync function getState(channelId: string, sessionKey?: string): Promise<ChannelState> {\n\tconst key = sessionKey ?? channelId;\n\tlet state = channelStates.get(key);\n\tif (!state) {\n\t\tconst channelDir = join(workingDir, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n\t\t\tstopRequested: false,\n\t\t\tlastAccessedAt: Date.now(),\n\t\t};\n\t\tchannelStates.set(key, state);\n\t} else {\n\t\tstate.lastAccessedAt = Date.now();\n\t}\n\treturn state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n *\n * Eviction rules:\n * - Never evict sessions that are currently running\n * - Evict sessions idle for more than IDLE_TIMEOUT_MS\n * - If still over MAX_SESSIONS, evict oldest idle sessions first\n */\nfunction evictIdleSessions(): void {\n\tconst now = Date.now();\n\n\t// First pass: evict sessions that are idle and past the timeout\n\tfor (const [key, state] of channelStates) {\n\t\tif (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n\t\t\tchannelStates.delete(key);\n\t\t}\n\t}\n\n\t// Second pass: if still over capacity, evict oldest idle sessions\n\tif (channelStates.size > MAX_SESSIONS) {\n\t\t// Collect all non-running sessions with their last access time\n\t\tconst idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n\t\tfor (const [key, state] of channelStates) {\n\t\t\tif (!state.running) {\n\t\t\t\tidleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n\t\t\t}\n\t\t}\n\n\t\t// Sort oldest first\n\t\tidleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n\t\t// Evict until under capacity\n\t\tconst toEvict = channelStates.size - MAX_SESSIONS;\n\t\tfor (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n\t\t\tchannelStates.delete(idleSessions[i].key);\n\t\t}\n\t}\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: MomHandler = {\n\tisRunning(sessionKey: string): boolean {\n\t\tconst state = channelStates.get(sessionKey);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(sessionKey: string, channelId: string, slack: SlackBot): Promise<void> {\n\t\tconst state = channelStates.get(sessionKey);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tconst ts = await slack.postMessage(channelId, \"_Stopping..._\");\n\t\t\tstate.stopMessageTs = ts; // Save for updating later\n\t\t} else {\n\t\t\tawait slack.postMessage(channelId, \"_Nothing running_\");\n\t\t}\n\t},\n\n\tasync handleEvent(event: SlackEvent, slack: SlackBot, isEvent?: boolean): Promise<void> {\n\t\t// Don't accept new events during shutdown\n\t\tif (isShuttingDown) {\n\t\t\tlog.logInfo(`[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst sessionKey = `${event.channel}:${event.thread_ts ?? event.ts}`;\n\t\tconst state = await getState(event.channel, sessionKey);\n\n\t\t// Start run\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tlog.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n\t\t// Wrap in-flight run tracking\n\t\tconst runPromise = (async () => {\n\t\t\ttry {\n\t\t\t\t// Create platform-agnostic adapter objects\n\t\t\t\tconst { message, responseCtx, platform } = createSlackAdapters(event, slack, isEvent);\n\n\t\t\t\t// Run the agent\n\t\t\t\tawait responseCtx.setTyping(true);\n\t\t\t\tawait responseCtx.setWorking(true);\n\t\t\t\tconst result = await state.runner.run(message, responseCtx, platform);\n\t\t\t\tawait responseCtx.setWorking(false);\n\n\t\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\t\tif (state.stopMessageTs) {\n\t\t\t\t\t\tawait slack.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n\t\t\t\t\t\tstate.stopMessageTs = undefined;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait slack.postMessage(event.channel, \"_Stopped_\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tlog.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t\t} finally {\n\t\t\t\tstate.running = false;\n\t\t\t\tstate.lastAccessedAt = Date.now();\n\t\t\t\tevictIdleSessions();\n\t\t\t}\n\t\t})();\n\n\t\tinFlightRuns.add(runPromise);\n\t\ttry {\n\t\t\tawait runPromise;\n\t\t} finally {\n\t\t\tinFlightRuns.delete(runPromise);\n\t\t}\n\t},\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nlog.logStartup(workingDir, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\n// Shared store for attachment downloads (also used per-channel in getState)\nconst sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n\nconst bot = new SlackBotClass(handler, {\n\tappToken: MOM_SLACK_APP_TOKEN,\n\tbotToken: MOM_SLACK_BOT_TOKEN,\n\tworkingDir,\n\tstore: sharedStore,\n});\n\n// Start events watcher\nconst eventsWatcher = createEventsWatcher(workingDir, bot);\neventsWatcher.start();\n\n// Handle shutdown\nprocess.on(\"SIGINT\", async () => {\n\tif (isShuttingDown) return; // Prevent duplicate signals\n\tisShuttingDown = true;\n\tlog.logInfo(\"Shutting down gracefully...\");\n\n\t// Wait for in-flight runs (max 30 seconds)\n\tconst timeout = Date.now() + 30000;\n\twhile (inFlightRuns.size > 0 && Date.now() < timeout) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t}\n\n\tif (inFlightRuns.size > 0) {\n\t\tlog.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n\t}\n\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", async () => {\n\tif (isShuttingDown) return; // Prevent duplicate signals\n\tisShuttingDown = true;\n\tlog.logInfo(\"Shutting down gracefully...\");\n\n\t// Wait for in-flight runs (max 30 seconds)\n\tconst timeout = Date.now() + 30000;\n\twhile (inFlightRuns.size > 0 && Date.now() < timeout) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t}\n\n\tif (inFlightRuns.size > 0) {\n\t\tlog.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n\t}\n\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nbot.start();\n"]}
@@ -0,0 +1,34 @@
1
+ export type SandboxConfig = {
2
+ type: "host";
3
+ } | {
4
+ type: "docker";
5
+ container: string;
6
+ };
7
+ export declare function parseSandboxArg(value: string): SandboxConfig;
8
+ export declare function validateSandbox(config: SandboxConfig): Promise<void>;
9
+ /**
10
+ * Create an executor that runs commands either on host or in Docker container
11
+ */
12
+ export declare function createExecutor(config: SandboxConfig): Executor;
13
+ export interface Executor {
14
+ /**
15
+ * Execute a bash command
16
+ */
17
+ exec(command: string, options?: ExecOptions): Promise<ExecResult>;
18
+ /**
19
+ * Get the workspace path prefix for this executor
20
+ * Host: returns the actual path
21
+ * Docker: returns /workspace
22
+ */
23
+ getWorkspacePath(hostPath: string): string;
24
+ }
25
+ export interface ExecOptions {
26
+ timeout?: number;
27
+ signal?: AbortSignal;
28
+ }
29
+ export interface ExecResult {
30
+ stdout: string;
31
+ stderr: string;
32
+ code: number;
33
+ }
34
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAc5D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B1E;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,QAAQ,CAK9D;AAED,MAAM,WAAW,QAAQ;IACxB;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAElE;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3C;AAED,MAAM,WAAW,WAAW;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb","sourcesContent":["import { spawn } from \"child_process\";\n\nexport type SandboxConfig = { type: \"host\" } | { type: \"docker\"; container: string };\n\nexport function parseSandboxArg(value: string): SandboxConfig {\n\tif (value === \"host\") {\n\t\treturn { type: \"host\" };\n\t}\n\tif (value.startsWith(\"docker:\")) {\n\t\tconst container = value.slice(\"docker:\".length);\n\t\tif (!container) {\n\t\t\tconsole.error(\"Error: docker sandbox requires container name (e.g., docker:mama-sandbox)\");\n\t\t\tprocess.exit(1);\n\t\t}\n\t\treturn { type: \"docker\", container };\n\t}\n\tconsole.error(`Error: Invalid sandbox type '${value}'. Use 'host' or 'docker:<container-name>'`);\n\tprocess.exit(1);\n}\n\nexport async function validateSandbox(config: SandboxConfig): Promise<void> {\n\tif (config.type === \"host\") {\n\t\treturn;\n\t}\n\n\t// Check if Docker is available\n\ttry {\n\t\tawait execSimple(\"docker\", [\"--version\"]);\n\t} catch {\n\t\tconsole.error(\"Error: Docker is not installed or not in PATH\");\n\t\tprocess.exit(1);\n\t}\n\n\t// Check if container exists and is running\n\ttry {\n\t\tconst result = await execSimple(\"docker\", [\"inspect\", \"-f\", \"{{.State.Running}}\", config.container]);\n\t\tif (result.trim() !== \"true\") {\n\t\t\tconsole.error(`Error: Container '${config.container}' is not running.`);\n\t\t\tconsole.error(`Start it with: docker start ${config.container}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t} catch {\n\t\tconsole.error(`Error: Container '${config.container}' does not exist.`);\n\t\tconsole.error(\"Create it with: ./docker.sh create <data-dir>\");\n\t\tprocess.exit(1);\n\t}\n\n\tconsole.log(` Docker container '${config.container}' is running.`);\n}\n\nfunction execSimple(cmd: string, args: string[]): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d;\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d;\n\t\t});\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (code === 0) resolve(stdout);\n\t\t\telse reject(new Error(stderr || `Exit code ${code}`));\n\t\t});\n\t});\n}\n\n/**\n * Create an executor that runs commands either on host or in Docker container\n */\nexport function createExecutor(config: SandboxConfig): Executor {\n\tif (config.type === \"host\") {\n\t\treturn new HostExecutor();\n\t}\n\treturn new DockerExecutor(config.container);\n}\n\nexport interface Executor {\n\t/**\n\t * Execute a bash command\n\t */\n\texec(command: string, options?: ExecOptions): Promise<ExecResult>;\n\n\t/**\n\t * Get the workspace path prefix for this executor\n\t * Host: returns the actual path\n\t * Docker: returns /workspace\n\t */\n\tgetWorkspacePath(hostPath: string): string;\n}\n\nexport interface ExecOptions {\n\ttimeout?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\nclass HostExecutor implements Executor {\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst shell = process.platform === \"win32\" ? \"cmd\" : \"sh\";\n\t\t\tconst shellArgs = process.platform === \"win32\" ? [\"/c\"] : [\"-c\"];\n\n\t\t\tconst child = spawn(shell, [...shellArgs, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\tconst timeoutHandle =\n\t\t\t\toptions?.timeout && options.timeout > 0\n\t\t\t\t\t? setTimeout(() => {\n\t\t\t\t\t\t\ttimedOut = true;\n\t\t\t\t\t\t\tkillProcessTree(child.pid!);\n\t\t\t\t\t\t}, options.timeout * 1000)\n\t\t\t\t\t: undefined;\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\n\t\t\tif (options?.signal) {\n\t\t\t\tif (options.signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\toptions.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchild.stdout?.on(\"data\", (data) => {\n\t\t\t\tstdout += data.toString();\n\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.stderr?.on(\"data\", (data) => {\n\t\t\t\tstderr += data.toString();\n\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\tif (options?.signal) {\n\t\t\t\t\toptions.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand aborted`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\treject(new Error(`${stdout}\\n${stderr}\\nCommand timed out after ${options?.timeout} seconds`.trim()));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tresolve({ stdout, stderr, code: code ?? 0 });\n\t\t\t});\n\t\t});\n\t}\n\n\tgetWorkspacePath(hostPath: string): string {\n\t\treturn hostPath;\n\t}\n}\n\nclass DockerExecutor implements Executor {\n\tconstructor(private container: string) {}\n\n\tasync exec(command: string, options?: ExecOptions): Promise<ExecResult> {\n\t\t// Wrap command for docker exec\n\t\tconst dockerCmd = `docker exec ${this.container} sh -c ${shellEscape(command)}`;\n\t\tconst hostExecutor = new HostExecutor();\n\t\treturn hostExecutor.exec(dockerCmd, options);\n\t}\n\n\tgetWorkspacePath(_hostPath: string): string {\n\t\t// Docker container sees /workspace\n\t\treturn \"/workspace\";\n\t}\n}\n\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t} else {\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction shellEscape(s: string): string {\n\t// Escape for passing to sh -c\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}