@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.10

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 (273) hide show
  1. package/README.md +171 -334
  2. package/dist/adapter.d.ts +36 -10
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +10 -5
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +349 -114
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +102 -31
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/shared.d.ts +71 -0
  14. package/dist/adapters/shared.d.ts.map +1 -0
  15. package/dist/adapters/shared.js +168 -0
  16. package/dist/adapters/shared.js.map +1 -0
  17. package/dist/adapters/slack/bot.d.ts +29 -22
  18. package/dist/adapters/slack/bot.d.ts.map +1 -1
  19. package/dist/adapters/slack/bot.js +620 -186
  20. package/dist/adapters/slack/bot.js.map +1 -1
  21. package/dist/adapters/slack/branch-manager.d.ts +22 -0
  22. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  23. package/dist/adapters/slack/branch-manager.js +97 -0
  24. package/dist/adapters/slack/branch-manager.js.map +1 -0
  25. package/dist/adapters/slack/context.d.ts +1 -1
  26. package/dist/adapters/slack/context.d.ts.map +1 -1
  27. package/dist/adapters/slack/context.js +136 -71
  28. package/dist/adapters/slack/context.js.map +1 -1
  29. package/dist/adapters/slack/session.d.ts +3 -0
  30. package/dist/adapters/slack/session.d.ts.map +1 -0
  31. package/dist/adapters/slack/session.js +16 -0
  32. package/dist/adapters/slack/session.js.map +1 -0
  33. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  34. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  35. package/dist/adapters/slack/tools/attach.js.map +1 -1
  36. package/dist/adapters/telegram/bot.d.ts +2 -0
  37. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  38. package/dist/adapters/telegram/bot.js +190 -123
  39. package/dist/adapters/telegram/bot.js.map +1 -1
  40. package/dist/adapters/telegram/context.d.ts.map +1 -1
  41. package/dist/adapters/telegram/context.js +57 -59
  42. package/dist/adapters/telegram/context.js.map +1 -1
  43. package/dist/adapters/telegram/html.d.ts +3 -0
  44. package/dist/adapters/telegram/html.d.ts.map +1 -0
  45. package/dist/adapters/telegram/html.js +98 -0
  46. package/dist/adapters/telegram/html.js.map +1 -0
  47. package/dist/agent.d.ts +9 -10
  48. package/dist/agent.d.ts.map +1 -1
  49. package/dist/agent.js +645 -555
  50. package/dist/agent.js.map +1 -1
  51. package/dist/commands/auto-reply.d.ts +16 -0
  52. package/dist/commands/auto-reply.d.ts.map +1 -0
  53. package/dist/commands/auto-reply.js +69 -0
  54. package/dist/commands/auto-reply.js.map +1 -0
  55. package/dist/commands/index.d.ts +5 -0
  56. package/dist/commands/index.d.ts.map +1 -0
  57. package/dist/commands/index.js +19 -0
  58. package/dist/commands/index.js.map +1 -0
  59. package/dist/commands/login.d.ts +5 -0
  60. package/dist/commands/login.d.ts.map +1 -0
  61. package/dist/commands/login.js +76 -0
  62. package/dist/commands/login.js.map +1 -0
  63. package/dist/commands/model.d.ts +14 -0
  64. package/dist/commands/model.d.ts.map +1 -0
  65. package/dist/commands/model.js +112 -0
  66. package/dist/commands/model.js.map +1 -0
  67. package/dist/commands/new.d.ts +9 -0
  68. package/dist/commands/new.d.ts.map +1 -0
  69. package/dist/commands/new.js +28 -0
  70. package/dist/commands/new.js.map +1 -0
  71. package/dist/commands/registry.d.ts +7 -0
  72. package/dist/commands/registry.d.ts.map +1 -0
  73. package/dist/commands/registry.js +14 -0
  74. package/dist/commands/registry.js.map +1 -0
  75. package/dist/commands/sandbox.d.ts +10 -0
  76. package/dist/commands/sandbox.d.ts.map +1 -0
  77. package/dist/commands/sandbox.js +88 -0
  78. package/dist/commands/sandbox.js.map +1 -0
  79. package/dist/commands/session-view.d.ts +5 -0
  80. package/dist/commands/session-view.d.ts.map +1 -0
  81. package/dist/commands/session-view.js +62 -0
  82. package/dist/commands/session-view.js.map +1 -0
  83. package/dist/commands/types.d.ts +41 -0
  84. package/dist/commands/types.d.ts.map +1 -0
  85. package/dist/commands/types.js +2 -0
  86. package/dist/commands/types.js.map +1 -0
  87. package/dist/commands/utils.d.ts +8 -0
  88. package/dist/commands/utils.d.ts.map +1 -0
  89. package/dist/commands/utils.js +14 -0
  90. package/dist/commands/utils.js.map +1 -0
  91. package/dist/config.d.ts +53 -7
  92. package/dist/config.d.ts.map +1 -1
  93. package/dist/config.js +320 -55
  94. package/dist/config.js.map +1 -1
  95. package/dist/context.d.ts +10 -42
  96. package/dist/context.d.ts.map +1 -1
  97. package/dist/context.js +15 -128
  98. package/dist/context.js.map +1 -1
  99. package/dist/events.d.ts +16 -5
  100. package/dist/events.d.ts.map +1 -1
  101. package/dist/events.js +127 -58
  102. package/dist/events.js.map +1 -1
  103. package/dist/execution-resolver.d.ts +24 -0
  104. package/dist/execution-resolver.d.ts.map +1 -0
  105. package/dist/execution-resolver.js +115 -0
  106. package/dist/execution-resolver.js.map +1 -0
  107. package/dist/file-guards.d.ts +6 -0
  108. package/dist/file-guards.d.ts.map +1 -0
  109. package/dist/file-guards.js +48 -0
  110. package/dist/file-guards.js.map +1 -0
  111. package/dist/fs-atomic.d.ts +10 -0
  112. package/dist/fs-atomic.d.ts.map +1 -0
  113. package/dist/fs-atomic.js +45 -0
  114. package/dist/fs-atomic.js.map +1 -0
  115. package/dist/index.d.ts +7 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +4 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/instrument.d.ts.map +1 -1
  120. package/dist/instrument.js +3 -3
  121. package/dist/instrument.js.map +1 -1
  122. package/dist/log.d.ts +3 -7
  123. package/dist/log.d.ts.map +1 -1
  124. package/dist/log.js +20 -45
  125. package/dist/log.js.map +1 -1
  126. package/dist/login/index.d.ts +41 -0
  127. package/dist/login/index.d.ts.map +1 -0
  128. package/dist/login/index.js +202 -0
  129. package/dist/login/index.js.map +1 -0
  130. package/dist/login/portal.d.ts +19 -0
  131. package/dist/login/portal.d.ts.map +1 -0
  132. package/dist/login/portal.js +1453 -0
  133. package/dist/login/portal.js.map +1 -0
  134. package/dist/login/session.d.ts +33 -0
  135. package/dist/login/session.d.ts.map +1 -0
  136. package/dist/login/session.js +68 -0
  137. package/dist/login/session.js.map +1 -0
  138. package/dist/main.d.ts.map +1 -1
  139. package/dist/main.js +229 -264
  140. package/dist/main.js.map +1 -1
  141. package/dist/provisioner.d.ts +79 -0
  142. package/dist/provisioner.d.ts.map +1 -0
  143. package/dist/provisioner.js +437 -0
  144. package/dist/provisioner.js.map +1 -0
  145. package/dist/runtime/conversation-orchestrator.d.ts +42 -0
  146. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  147. package/dist/runtime/conversation-orchestrator.js +150 -0
  148. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  149. package/dist/runtime/index.d.ts +2 -0
  150. package/dist/runtime/index.d.ts.map +1 -0
  151. package/dist/runtime/index.js +2 -0
  152. package/dist/runtime/index.js.map +1 -0
  153. package/dist/runtime/session-runtime.d.ts +27 -0
  154. package/dist/runtime/session-runtime.d.ts.map +1 -0
  155. package/dist/runtime/session-runtime.js +211 -0
  156. package/dist/runtime/session-runtime.js.map +1 -0
  157. package/dist/sandbox/cloudflare.d.ts +15 -0
  158. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  159. package/dist/sandbox/cloudflare.js +137 -0
  160. package/dist/sandbox/cloudflare.js.map +1 -0
  161. package/dist/sandbox/container.d.ts +16 -0
  162. package/dist/sandbox/container.d.ts.map +1 -0
  163. package/dist/sandbox/container.js +126 -0
  164. package/dist/sandbox/container.js.map +1 -0
  165. package/dist/sandbox/errors.d.ts +6 -0
  166. package/dist/sandbox/errors.d.ts.map +1 -0
  167. package/dist/sandbox/errors.js +11 -0
  168. package/dist/sandbox/errors.js.map +1 -0
  169. package/dist/sandbox/firecracker.d.ts +17 -0
  170. package/dist/sandbox/firecracker.d.ts.map +1 -0
  171. package/dist/sandbox/firecracker.js +212 -0
  172. package/dist/sandbox/firecracker.js.map +1 -0
  173. package/dist/sandbox/host.d.ts +11 -0
  174. package/dist/sandbox/host.d.ts.map +1 -0
  175. package/dist/sandbox/host.js +89 -0
  176. package/dist/sandbox/host.js.map +1 -0
  177. package/dist/sandbox/image.d.ts +5 -0
  178. package/dist/sandbox/image.d.ts.map +1 -0
  179. package/dist/sandbox/image.js +30 -0
  180. package/dist/sandbox/image.js.map +1 -0
  181. package/dist/sandbox/index.d.ts +22 -0
  182. package/dist/sandbox/index.d.ts.map +1 -0
  183. package/dist/sandbox/index.js +54 -0
  184. package/dist/sandbox/index.js.map +1 -0
  185. package/dist/sandbox/path-context.d.ts +4 -0
  186. package/dist/sandbox/path-context.d.ts.map +1 -0
  187. package/dist/sandbox/path-context.js +20 -0
  188. package/dist/sandbox/path-context.js.map +1 -0
  189. package/dist/sandbox/types.d.ts +67 -0
  190. package/dist/sandbox/types.d.ts.map +1 -0
  191. package/dist/sandbox/types.js +2 -0
  192. package/dist/sandbox/types.js.map +1 -0
  193. package/dist/sandbox/utils.d.ts +4 -0
  194. package/dist/sandbox/utils.d.ts.map +1 -0
  195. package/dist/sandbox/utils.js +51 -0
  196. package/dist/sandbox/utils.js.map +1 -0
  197. package/dist/sandbox.d.ts +1 -39
  198. package/dist/sandbox.d.ts.map +1 -1
  199. package/dist/sandbox.js +1 -286
  200. package/dist/sandbox.js.map +1 -1
  201. package/dist/sentry.d.ts +2 -2
  202. package/dist/sentry.d.ts.map +1 -1
  203. package/dist/sentry.js +6 -4
  204. package/dist/sentry.js.map +1 -1
  205. package/dist/session-policy.d.ts +13 -0
  206. package/dist/session-policy.d.ts.map +1 -0
  207. package/dist/session-policy.js +23 -0
  208. package/dist/session-policy.js.map +1 -0
  209. package/dist/session-store.d.ts +35 -8
  210. package/dist/session-store.d.ts.map +1 -1
  211. package/dist/session-store.js +182 -23
  212. package/dist/session-store.js.map +1 -1
  213. package/dist/session-view/command.d.ts +5 -0
  214. package/dist/session-view/command.d.ts.map +1 -0
  215. package/dist/session-view/command.js +11 -0
  216. package/dist/session-view/command.js.map +1 -0
  217. package/dist/session-view/portal.d.ts +16 -0
  218. package/dist/session-view/portal.d.ts.map +1 -0
  219. package/dist/session-view/portal.js +1742 -0
  220. package/dist/session-view/portal.js.map +1 -0
  221. package/dist/session-view/service.d.ts +34 -0
  222. package/dist/session-view/service.d.ts.map +1 -0
  223. package/dist/session-view/service.js +427 -0
  224. package/dist/session-view/service.js.map +1 -0
  225. package/dist/session-view/store.d.ts +18 -0
  226. package/dist/session-view/store.d.ts.map +1 -0
  227. package/dist/session-view/store.js +39 -0
  228. package/dist/session-view/store.js.map +1 -0
  229. package/dist/store.d.ts +4 -7
  230. package/dist/store.d.ts.map +1 -1
  231. package/dist/store.js +26 -52
  232. package/dist/store.js.map +1 -1
  233. package/dist/tool-diagnostics.d.ts +2 -0
  234. package/dist/tool-diagnostics.d.ts.map +1 -0
  235. package/dist/tool-diagnostics.js +7 -0
  236. package/dist/tool-diagnostics.js.map +1 -0
  237. package/dist/tools/bash.d.ts +1 -1
  238. package/dist/tools/bash.d.ts.map +1 -1
  239. package/dist/tools/bash.js.map +1 -1
  240. package/dist/tools/edit.d.ts +1 -1
  241. package/dist/tools/edit.d.ts.map +1 -1
  242. package/dist/tools/edit.js.map +1 -1
  243. package/dist/tools/event.d.ts +62 -0
  244. package/dist/tools/event.d.ts.map +1 -0
  245. package/dist/tools/event.js +138 -0
  246. package/dist/tools/event.js.map +1 -0
  247. package/dist/tools/index.d.ts +8 -2
  248. package/dist/tools/index.d.ts.map +1 -1
  249. package/dist/tools/index.js +5 -1
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/read.d.ts +1 -1
  252. package/dist/tools/read.d.ts.map +1 -1
  253. package/dist/tools/read.js.map +1 -1
  254. package/dist/tools/write.d.ts +1 -1
  255. package/dist/tools/write.d.ts.map +1 -1
  256. package/dist/tools/write.js.map +1 -1
  257. package/dist/trigger.d.ts +31 -0
  258. package/dist/trigger.d.ts.map +1 -0
  259. package/dist/trigger.js +98 -0
  260. package/dist/trigger.js.map +1 -0
  261. package/dist/ui-copy.d.ts +12 -0
  262. package/dist/ui-copy.d.ts.map +1 -0
  263. package/dist/ui-copy.js +36 -0
  264. package/dist/ui-copy.js.map +1 -0
  265. package/dist/vault-routing.d.ts +4 -0
  266. package/dist/vault-routing.d.ts.map +1 -0
  267. package/dist/vault-routing.js +16 -0
  268. package/dist/vault-routing.js.map +1 -0
  269. package/dist/vault.d.ts +72 -0
  270. package/dist/vault.d.ts.map +1 -0
  271. package/dist/vault.js +264 -0
  272. package/dist/vault.js.map +1 -0
  273. package/package.json +16 -13
package/dist/main.js CHANGED
@@ -1,19 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import "./instrument.js";
3
3
  import { join, resolve } from "path";
4
- import { readFileSync } from "fs";
4
+ import { mkdirSync, statSync, writeFileSync } from "fs";
5
+ import { homedir } from "os";
5
6
  import { fileURLToPath } from "url";
6
7
  import { dirname, join as pathJoin } from "path";
7
8
  import { DiscordBot } from "./adapters/discord/index.js";
8
9
  import { TelegramBot } from "./adapters/telegram/index.js";
9
10
  import { SlackBot as SlackBotClass } from "./adapters/slack/index.js";
10
- import { createRunner } from "./agent.js";
11
- import { createManagedSessionFile, createManagedSessionFileAtPath, getSessionDir, getThreadSessionFile, } from "./session-store.js";
12
11
  import { downloadChannel } from "./download.js";
13
12
  import { createEventsWatcher } from "./events.js";
14
13
  import * as log from "./log.js";
15
- import { parseSandboxArg, validateSandbox } from "./sandbox.js";
16
- import { addLifecycleBreadcrumb, applyRunScope } from "./sentry.js";
14
+ import { startLinkServer } from "./login/portal.js";
15
+ import { InMemoryLinkTokenStore } from "./login/session.js";
16
+ import { InMemorySessionViewTokenStore } from "./session-view/store.js";
17
+ import { DockerContainerManager } from "./provisioner.js";
18
+ import { createGlobalSettingsFile, loadAgentConfig, MissingGlobalSettingsError } from "./config.js";
19
+ import { ensureDirExists, isRecord, readJsonFileIfExists } from "./file-guards.js";
20
+ import { SandboxError, parseSandboxArg, validateSandbox } from "./sandbox.js";
21
+ import { FileVaultManager } from "./vault.js";
22
+ import { createSessionRuntime } from "./runtime/index.js";
17
23
  import { ChannelStore } from "./store.js";
18
24
  import * as Sentry from "@sentry/node";
19
25
  // ============================================================================
@@ -28,38 +34,50 @@ function getVersion() {
28
34
  pathJoin(process.cwd(), "package.json"),
29
35
  ];
30
36
  for (const pkgPath of possiblePaths) {
31
- try {
32
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
33
- if (pkg.version)
34
- return pkg.version;
35
- }
36
- catch {
37
- // Continue to next path
38
- }
37
+ const pkg = readJsonFileIfExists(pkgPath, (value) => isRecord(value), () => "Ignoring package.json while resolving version");
38
+ if (typeof pkg?.version === "string" && pkg.version)
39
+ return pkg.version;
39
40
  }
40
41
  return "unknown";
41
42
  }
42
- const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
43
- const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
44
- const MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;
45
- const MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;
43
+ const MAMA_SLACK_APP_TOKEN = process.env.MAMA_SLACK_APP_TOKEN;
44
+ const MAMA_SLACK_BOT_TOKEN = process.env.MAMA_SLACK_BOT_TOKEN;
45
+ const MAMA_TELEGRAM_BOT_TOKEN = process.env.MAMA_TELEGRAM_BOT_TOKEN;
46
+ const MAMA_DISCORD_BOT_TOKEN = process.env.MAMA_DISCORD_BOT_TOKEN;
47
+ const MAMA_LINK_URL = process.env.MAMA_LINK_URL;
48
+ const MAMA_LINK_PORT = process.env.MAMA_LINK_PORT
49
+ ? parseInt(process.env.MAMA_LINK_PORT, 10)
50
+ : MAMA_LINK_URL
51
+ ? 8181
52
+ : undefined;
46
53
  function parseArgs() {
47
54
  const args = process.argv.slice(2);
48
55
  let sandbox = { type: "host" };
49
56
  let workingDir;
57
+ let stateDirArg;
50
58
  let downloadChannelId;
59
+ let showOnboard = false;
51
60
  let showVersion = false;
52
61
  for (let i = 0; i < args.length; i++) {
53
62
  const arg = args[i];
54
63
  if (arg === "--version" || arg === "-v" || arg === "-V") {
55
64
  showVersion = true;
56
65
  }
66
+ else if (arg === "--onboard") {
67
+ showOnboard = true;
68
+ }
57
69
  else if (arg.startsWith("--sandbox=")) {
58
70
  sandbox = parseSandboxArg(arg.slice("--sandbox=".length));
59
71
  }
60
72
  else if (arg === "--sandbox") {
61
73
  sandbox = parseSandboxArg(args[++i] || "");
62
74
  }
75
+ else if (arg.startsWith("--state-dir=")) {
76
+ stateDirArg = arg.slice("--state-dir=".length);
77
+ }
78
+ else if (arg === "--state-dir") {
79
+ stateDirArg = args[++i];
80
+ }
63
81
  else if (arg.startsWith("--download=")) {
64
82
  downloadChannelId = arg.slice("--download=".length);
65
83
  }
@@ -72,283 +90,225 @@ function parseArgs() {
72
90
  }
73
91
  return {
74
92
  workingDir: workingDir ? resolve(workingDir) : undefined,
93
+ stateDir: stateDirArg ? resolve(stateDirArg) : undefined,
75
94
  sandbox,
76
95
  downloadChannel: downloadChannelId,
96
+ showOnboard,
77
97
  showVersion,
78
98
  };
79
99
  }
80
- const parsedArgs = parseArgs();
100
+ const WORLD_WRITABLE_MODE = 0o002;
101
+ function ensureSecureStateDir(path) {
102
+ let stat;
103
+ try {
104
+ stat = statSync(path);
105
+ }
106
+ catch (err) {
107
+ const code = err.code;
108
+ if (code === "ENOENT") {
109
+ mkdirSync(path, { recursive: true, mode: 0o700 });
110
+ return;
111
+ }
112
+ console.error(`Error: cannot access --state-dir ${path}: ${err.message}`);
113
+ process.exit(1);
114
+ }
115
+ if (!stat.isDirectory()) {
116
+ console.error(`Error: --state-dir ${path} exists but is not a directory`);
117
+ process.exit(1);
118
+ }
119
+ if (stat.mode & WORLD_WRITABLE_MODE) {
120
+ console.error(`Error: --state-dir ${path} is world-writable (mode ${(stat.mode & 0o777).toString(8)}). ` +
121
+ `Credentials stored there would be exposed to other local users. ` +
122
+ `Fix with: chmod 0700 ${path}`);
123
+ process.exit(1);
124
+ }
125
+ const euid = typeof process.geteuid === "function" ? process.geteuid() : undefined;
126
+ if (euid !== undefined && stat.uid !== euid) {
127
+ console.error(`Error: --state-dir ${path} is owned by uid ${stat.uid} but mama is running as uid ${euid}. ` +
128
+ `Run mama as the directory owner or point --state-dir at a directory you own.`);
129
+ process.exit(1);
130
+ }
131
+ }
132
+ function handleStartupError(error) {
133
+ if (error instanceof SandboxError) {
134
+ for (const line of error.formatForCli()) {
135
+ console.error(line);
136
+ }
137
+ process.exit(1);
138
+ }
139
+ if (error instanceof MissingGlobalSettingsError) {
140
+ console.error(`Missing global settings: ${error.settingsPath}`);
141
+ console.error("");
142
+ console.error("Run onboarding to create it:");
143
+ console.error(` mama --onboard --state-dir ${stateDir}`);
144
+ console.error("");
145
+ console.error("Then review the generated settings.json and start mama again.");
146
+ process.exit(1);
147
+ }
148
+ if (error instanceof Error) {
149
+ console.error(`Error: ${error.message}`);
150
+ process.exit(1);
151
+ }
152
+ console.error(String(error));
153
+ process.exit(1);
154
+ }
155
+ let parsedArgs;
156
+ try {
157
+ parsedArgs = parseArgs();
158
+ }
159
+ catch (error) {
160
+ handleStartupError(error);
161
+ }
81
162
  // Handle --version
82
163
  if (parsedArgs.showVersion) {
83
164
  console.log(getVersion());
84
165
  process.exit(0);
85
166
  }
167
+ // Handle --onboard mode
168
+ if (parsedArgs.showOnboard) {
169
+ const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
170
+ process.env.MAMA_STATE_DIR = stateDir;
171
+ ensureSecureStateDir(stateDir);
172
+ try {
173
+ const settingsPath = createGlobalSettingsFile(stateDir);
174
+ console.log(`Created global settings at ${settingsPath}`);
175
+ console.log("Review the file, then start mama with your working directory.");
176
+ process.exit(0);
177
+ }
178
+ catch (err) {
179
+ console.error(err instanceof Error ? err.message : String(err));
180
+ process.exit(1);
181
+ }
182
+ }
86
183
  // Handle --download mode (Slack only)
87
184
  if (parsedArgs.downloadChannel) {
88
- if (!MOM_SLACK_BOT_TOKEN) {
89
- console.error("Missing env: MOM_SLACK_BOT_TOKEN");
185
+ if (!MAMA_SLACK_BOT_TOKEN) {
186
+ console.error("Missing env: MAMA_SLACK_BOT_TOKEN");
90
187
  process.exit(1);
91
188
  }
92
- await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);
189
+ await downloadChannel(parsedArgs.downloadChannel, MAMA_SLACK_BOT_TOKEN);
93
190
  process.exit(0);
94
191
  }
95
192
  // Normal bot mode - require working dir
96
193
  if (!parsedArgs.workingDir) {
97
- console.error("Usage: mama [--sandbox=host|docker:<name>|firecracker:<vm-id>:<host-path>] <working-directory>");
194
+ console.error("Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>|cloudflare:<sandbox-id>] <working-directory>");
195
+ console.error(" mama --onboard [--state-dir=<dir>]");
98
196
  console.error(" mama --download <channel-id>");
99
197
  process.exit(1);
100
198
  }
101
199
  const { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };
200
+ const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
201
+ process.env.MAMA_STATE_DIR = stateDir;
202
+ ensureSecureStateDir(stateDir);
102
203
  // Validate platform tokens
103
- const hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);
104
- const hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;
105
- const hasDiscord = !!MOM_DISCORD_BOT_TOKEN;
204
+ const hasSlack = !!(MAMA_SLACK_APP_TOKEN && MAMA_SLACK_BOT_TOKEN);
205
+ const hasTelegram = !!MAMA_TELEGRAM_BOT_TOKEN;
206
+ const hasDiscord = !!MAMA_DISCORD_BOT_TOKEN;
106
207
  if (!hasSlack && !hasTelegram && !hasDiscord) {
107
208
  console.error("No platform tokens found. Set one of:\n" +
108
- " Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\n" +
109
- " Telegram: MOM_TELEGRAM_BOT_TOKEN\n" +
110
- " Discord: MOM_DISCORD_BOT_TOKEN");
209
+ " Slack: MAMA_SLACK_APP_TOKEN + MAMA_SLACK_BOT_TOKEN\n" +
210
+ " Telegram: MAMA_TELEGRAM_BOT_TOKEN\n" +
211
+ " Discord: MAMA_DISCORD_BOT_TOKEN");
111
212
  process.exit(1);
112
213
  }
113
- await validateSandbox(sandbox);
114
- const channelStates = new Map();
115
- /** Track in-flight runs for graceful shutdown */
116
- const inFlightRuns = new Set();
117
- /** Flag to stop accepting new events during shutdown */
118
- let isShuttingDown = false;
119
- /** Maximum number of cached sessions */
120
- const MAX_SESSIONS = 500;
121
- /** Idle timeout before a non-running session can be evicted (1 hour) */
122
- const IDLE_TIMEOUT_MS = 3600000;
123
- async function getState(channelId, sessionKey) {
124
- const key = sessionKey ?? channelId;
125
- let state = channelStates.get(key);
126
- if (!state) {
127
- const channelDir = join(workingDir, channelId);
128
- state = {
129
- running: false,
130
- runner: await createRunner(sandbox, key, channelId, channelDir, workingDir),
131
- stopRequested: false,
132
- lastAccessedAt: Date.now(),
133
- };
134
- channelStates.set(key, state);
214
+ try {
215
+ await validateSandbox(sandbox);
216
+ }
217
+ catch (error) {
218
+ handleStartupError(error);
219
+ }
220
+ const vaultManager = new FileVaultManager(stateDir);
221
+ if (vaultManager.isEnabled()) {
222
+ console.log(sandbox.type === "container"
223
+ ? " Vault system enabled. Container vault active."
224
+ : sandbox.type === "image" || sandbox.type === "firecracker" || sandbox.type === "cloudflare"
225
+ ? " Vault system enabled. Conversation-scoped credential routing active."
226
+ : " Vault system enabled. Host mode will not inject vault env.");
227
+ }
228
+ const startupConfig = (() => {
229
+ try {
230
+ return loadAgentConfig();
135
231
  }
136
- else {
137
- state.lastAccessedAt = Date.now();
232
+ catch (error) {
233
+ handleStartupError(error);
138
234
  }
139
- return state;
140
- }
141
- /**
142
- * Evict idle sessions from channelStates to bound memory usage.
143
- * Called after each handleEvent completes.
144
- */
145
- function evictIdleSessions() {
146
- const now = Date.now();
147
- for (const [key, state] of channelStates) {
148
- if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
149
- channelStates.delete(key);
150
- }
235
+ })();
236
+ const sandboxLimits = startupConfig.sandboxCpus || startupConfig.sandboxMemory
237
+ ? { cpus: startupConfig.sandboxCpus, memory: startupConfig.sandboxMemory }
238
+ : undefined;
239
+ const sandboxBoostLimits = startupConfig.sandboxBoostCpus || startupConfig.sandboxBoostMemory
240
+ ? { cpus: startupConfig.sandboxBoostCpus, memory: startupConfig.sandboxBoostMemory }
241
+ : undefined;
242
+ const provisioner = sandbox.type === "image"
243
+ ? new DockerContainerManager(sandbox.image, {
244
+ limits: sandboxLimits,
245
+ boostLimits: sandboxBoostLimits,
246
+ })
247
+ : undefined;
248
+ if (sandbox.type === "image") {
249
+ ensureDirExists(join(workingDir, "skills"));
250
+ ensureDirExists(join(workingDir, "events"));
251
+ try {
252
+ writeFileSync(join(workingDir, "MEMORY.md"), "", { flag: "wx" });
151
253
  }
152
- if (channelStates.size > MAX_SESSIONS) {
153
- const idleSessions = [];
154
- for (const [key, state] of channelStates) {
155
- if (!state.running) {
156
- idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });
157
- }
158
- }
159
- idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
160
- const toEvict = channelStates.size - MAX_SESSIONS;
161
- for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
162
- channelStates.delete(idleSessions[i].key);
163
- }
254
+ catch (err) {
255
+ if (err.code !== "EEXIST")
256
+ throw err;
164
257
  }
165
258
  }
166
- // ============================================================================
167
- // Handler
168
- // ============================================================================
169
- const handler = {
170
- isRunning(sessionKey) {
171
- const state = channelStates.get(sessionKey);
172
- return !!state?.running;
173
- },
174
- getRunningSessions() {
175
- const sessions = [];
176
- for (const [sessionKey, state] of channelStates) {
177
- if (state.running && state.startedAt) {
178
- // Get current step from runner
179
- const currentStep = state.runner.getCurrentStep();
180
- sessions.push({
181
- sessionKey,
182
- startedAt: state.startedAt,
183
- lastActivityAt: state.lastActivityAt,
184
- currentTool: currentStep?.label || currentStep?.toolName,
185
- });
186
- }
187
- }
188
- return sessions;
189
- },
190
- async handleStop(sessionKey, channelId, bot) {
191
- const state = channelStates.get(sessionKey);
192
- if (state?.running) {
193
- state.stopRequested = true;
194
- state.runner.abort();
195
- const ts = await bot.postMessage(channelId, "_Stopping..._");
196
- state.stopMessageTs = ts;
197
- }
198
- else {
199
- await bot.postMessage(channelId, "_Nothing running_");
200
- }
201
- },
202
- forceStop(sessionKey) {
203
- const state = channelStates.get(sessionKey);
204
- if (state?.running) {
205
- log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);
206
- state.stopRequested = true;
207
- state.runner.abort();
208
- state.running = false;
209
- }
210
- },
211
- async handleNew(sessionKey, channelId, bot) {
212
- const state = channelStates.get(sessionKey);
213
- if (state?.running) {
214
- state.stopRequested = true;
215
- state.runner.abort();
216
- }
217
- // Channel sessions rotate via current pointer. Thread sessions reset in place.
218
- const channelDir = join(workingDir, channelId);
219
- if (sessionKey.includes(":")) {
220
- createManagedSessionFileAtPath(getThreadSessionFile(channelDir, sessionKey), channelDir);
221
- }
222
- else {
223
- createManagedSessionFile(getSessionDir(channelDir, sessionKey), channelDir);
224
- }
225
- // Remove from in-memory cache
226
- channelStates.delete(sessionKey);
227
- log.logInfo(`[${channelId}] Session reset: ${sessionKey}`);
228
- await bot.postMessage(channelId, "Conversation reset. Send a new message to start fresh.");
229
- },
230
- async handleEvent(event, bot, adapters, _isEvent) {
231
- // Don't accept new events during shutdown
232
- if (isShuttingDown) {
233
- log.logInfo(`[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
234
- return;
235
- }
236
- const sessionKey = event.sessionKey ?? `${event.channel}:${event.thread_ts ?? event.ts}`;
237
- const state = await getState(event.channel, sessionKey);
238
- // Start run
239
- state.running = true;
240
- state.stopRequested = false;
241
- state.startedAt = Date.now();
242
- state.lastActivityAt = Date.now();
243
- log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);
244
- // Wrap in-flight run tracking
245
- Sentry.metrics.count("agent.run.started", 1, {
246
- attributes: { channel: event.channel },
247
- });
248
- Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size + 1);
249
- const runPromise = Sentry.startSpan({ name: "agent.run", op: "agent", attributes: { channelId: event.channel, sessionKey } }, async () => {
250
- return Sentry.withScope(async (scope) => {
251
- const { message, responseCtx, platform } = adapters;
252
- applyRunScope(scope, {
253
- channelId: event.channel,
254
- sessionKey,
255
- messageId: message.id,
256
- platform: platform.name,
257
- userId: message.userId,
258
- userName: message.userName,
259
- threadTs: message.threadTs,
260
- isEvent: _isEvent,
261
- });
262
- addLifecycleBreadcrumb("agent.run.started", {
263
- channel_id: event.channel,
264
- platform: platform.name,
265
- has_attachments: (message.attachments?.length ?? 0) > 0,
266
- });
267
- try {
268
- await responseCtx.setTyping(true);
269
- await responseCtx.setWorking(true);
270
- const result = await state.runner.run(message, responseCtx, platform);
271
- await responseCtx.setWorking(false);
272
- const durationMs = Date.now() - state.startedAt;
273
- Sentry.metrics.distribution("agent.run.duration", durationMs, {
274
- unit: "millisecond",
275
- attributes: {
276
- channel: event.channel,
277
- platform: platform.name,
278
- stop_reason: result.stopReason,
279
- },
280
- });
281
- Sentry.metrics.count("agent.run.completed", 1, {
282
- attributes: {
283
- channel: event.channel,
284
- platform: platform.name,
285
- stop_reason: result.stopReason,
286
- },
287
- });
288
- addLifecycleBreadcrumb("agent.run.completed", {
289
- channel_id: event.channel,
290
- platform: platform.name,
291
- stop_reason: result.stopReason,
292
- duration_ms: durationMs,
293
- });
294
- if (result.stopReason === "aborted" && state.stopRequested) {
295
- if (state.stopMessageTs) {
296
- await bot.updateMessage(event.channel, state.stopMessageTs, "_Stopped_");
297
- state.stopMessageTs = undefined;
298
- }
299
- else {
300
- await bot.postMessage(event.channel, "_Stopped_");
301
- }
302
- }
303
- }
304
- catch (err) {
305
- scope.setContext("agent_run_error", {
306
- channelId: event.channel,
307
- sessionKey,
308
- platform: adapters.platform.name,
309
- messageId: adapters.message.id,
310
- threadTs: adapters.message.threadTs,
311
- });
312
- Sentry.captureException(err);
313
- Sentry.metrics.count("agent.run.errors", 1, {
314
- attributes: { channel: event.channel, platform: adapters.platform.name },
315
- });
316
- log.logWarning(`[${event.channel}] Run error`, err instanceof Error ? err.message : String(err));
317
- }
318
- finally {
319
- state.running = false;
320
- state.lastAccessedAt = Date.now();
321
- Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size - 1);
322
- evictIdleSessions();
323
- }
324
- });
325
- });
326
- inFlightRuns.add(runPromise);
327
- try {
328
- await runPromise;
329
- }
330
- finally {
331
- inFlightRuns.delete(runPromise);
332
- }
333
- },
334
- };
259
+ const linkTokenStore = new InMemoryLinkTokenStore();
260
+ const sessionViewTokenStore = new InMemorySessionViewTokenStore();
261
+ setInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();
262
+ setInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();
263
+ function portalBaseUrl() {
264
+ if (MAMA_LINK_URL)
265
+ return MAMA_LINK_URL.replace(/\/+$/, "");
266
+ if (MAMA_LINK_PORT)
267
+ return `http://localhost:${MAMA_LINK_PORT}`;
268
+ return undefined;
269
+ }
270
+ /** Idle timeout for managed image containers (10 minutes) */
271
+ const IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
272
+ if (provisioner) {
273
+ await provisioner.reconcile();
274
+ await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);
275
+ setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();
276
+ }
277
+ const handler = createSessionRuntime({
278
+ workingDir,
279
+ sandbox,
280
+ vaultManager,
281
+ provisioner,
282
+ linkTokenStore,
283
+ sessionViewTokenStore,
284
+ portalBaseUrl: portalBaseUrl(),
285
+ });
335
286
  // ============================================================================
336
287
  // Start
337
288
  // ============================================================================
338
289
  const sandboxDesc = sandbox.type === "host"
339
290
  ? "host"
340
- : sandbox.type === "docker"
341
- ? `docker:${sandbox.container}`
342
- : `firecracker:${sandbox.vmId}`;
291
+ : sandbox.type === "container"
292
+ ? `container:${sandbox.container}`
293
+ : sandbox.type === "image"
294
+ ? `image:${sandbox.image}`
295
+ : sandbox.type === "firecracker"
296
+ ? `firecracker:${sandbox.vmId}`
297
+ : `cloudflare:${sandbox.sandboxId}`;
343
298
  log.logStartup(workingDir, sandboxDesc);
344
299
  // Create platform bots
345
300
  const bots = [];
346
301
  const botsByPlatform = {};
347
302
  if (hasSlack) {
348
- const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN });
303
+ const slackBotToken = MAMA_SLACK_BOT_TOKEN;
304
+ const slackAppToken = MAMA_SLACK_APP_TOKEN;
305
+ if (!slackBotToken || !slackAppToken) {
306
+ throw new Error("Slack startup requires both MAMA_SLACK_APP_TOKEN and MAMA_SLACK_BOT_TOKEN");
307
+ }
308
+ const sharedStore = new ChannelStore({ workingDir, botToken: slackBotToken });
349
309
  const slackBot = new SlackBotClass(handler, {
350
- appToken: MOM_SLACK_APP_TOKEN,
351
- botToken: MOM_SLACK_BOT_TOKEN,
310
+ appToken: slackAppToken,
311
+ botToken: slackBotToken,
352
312
  workingDir,
353
313
  store: sharedStore,
354
314
  });
@@ -357,8 +317,12 @@ if (hasSlack) {
357
317
  log.logInfo("Platform: Slack");
358
318
  }
359
319
  if (hasTelegram) {
320
+ const telegramToken = MAMA_TELEGRAM_BOT_TOKEN;
321
+ if (!telegramToken) {
322
+ throw new Error("Telegram startup requires MAMA_TELEGRAM_BOT_TOKEN");
323
+ }
360
324
  const telegramBot = new TelegramBot(handler, {
361
- token: MOM_TELEGRAM_BOT_TOKEN,
325
+ token: telegramToken,
362
326
  workingDir,
363
327
  });
364
328
  bots.push(telegramBot);
@@ -366,14 +330,25 @@ if (hasTelegram) {
366
330
  log.logInfo("Platform: Telegram");
367
331
  }
368
332
  if (hasDiscord) {
333
+ const discordToken = MAMA_DISCORD_BOT_TOKEN;
334
+ if (!discordToken) {
335
+ throw new Error("Discord startup requires MAMA_DISCORD_BOT_TOKEN");
336
+ }
369
337
  const discordBot = new DiscordBot(handler, {
370
- token: MOM_DISCORD_BOT_TOKEN,
338
+ token: discordToken,
371
339
  workingDir,
372
340
  });
373
341
  bots.push(discordBot);
374
342
  botsByPlatform.discord = discordBot;
375
343
  log.logInfo("Platform: Discord");
376
344
  }
345
+ if (MAMA_LINK_PORT) {
346
+ startLinkServer(MAMA_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
347
+ const bot = botsByPlatform[platform];
348
+ if (bot)
349
+ await bot.postMessage(conversationId, message);
350
+ }, sessionViewTokenStore, { handler, botsByPlatform });
351
+ }
377
352
  // Start events watcher with explicit platform routing
378
353
  const eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);
379
354
  const slackBot = botsByPlatform.slack;
@@ -383,17 +358,7 @@ if (slackBot) {
383
358
  eventsWatcher.start();
384
359
  // Handle shutdown
385
360
  async function shutdown() {
386
- if (isShuttingDown)
387
- return;
388
- isShuttingDown = true;
389
- log.logInfo("Shutting down gracefully...");
390
- const timeout = Date.now() + 30000;
391
- while (inFlightRuns.size > 0 && Date.now() < timeout) {
392
- await new Promise((resolve) => setTimeout(resolve, 500));
393
- }
394
- if (inFlightRuns.size > 0) {
395
- log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);
396
- }
361
+ await handler.shutdown();
397
362
  eventsWatcher.stop();
398
363
  await Sentry.close(5000);
399
364
  process.exit(0);