@geminixiang/mama 0.2.0-beta.2 → 0.2.0-beta.21

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 (264) hide show
  1. package/README.md +157 -391
  2. package/dist/adapter.d.ts +31 -7
  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 +347 -115
  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 +118 -25
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/shared.d.ts +91 -0
  14. package/dist/adapters/shared.d.ts.map +1 -0
  15. package/dist/adapters/shared.js +191 -0
  16. package/dist/adapters/shared.js.map +1 -0
  17. package/dist/adapters/slack/bot.d.ts +21 -22
  18. package/dist/adapters/slack/bot.d.ts.map +1 -1
  19. package/dist/adapters/slack/bot.js +530 -221
  20. package/dist/adapters/slack/bot.js.map +1 -1
  21. package/dist/adapters/slack/branch-manager.d.ts +28 -0
  22. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  23. package/dist/adapters/slack/branch-manager.js +107 -0
  24. package/dist/adapters/slack/branch-manager.js.map +1 -0
  25. package/dist/adapters/slack/context.d.ts +4 -1
  26. package/dist/adapters/slack/context.d.ts.map +1 -1
  27. package/dist/adapters/slack/context.js +193 -75
  28. package/dist/adapters/slack/context.js.map +1 -1
  29. package/dist/adapters/slack/session.d.ts +38 -0
  30. package/dist/adapters/slack/session.d.ts.map +1 -0
  31. package/dist/adapters/slack/session.js +66 -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.map +1 -1
  37. package/dist/adapters/telegram/bot.js +140 -153
  38. package/dist/adapters/telegram/bot.js.map +1 -1
  39. package/dist/adapters/telegram/context.d.ts +1 -1
  40. package/dist/adapters/telegram/context.d.ts.map +1 -1
  41. package/dist/adapters/telegram/context.js +74 -20
  42. package/dist/adapters/telegram/context.js.map +1 -1
  43. package/dist/agent.d.ts +13 -3
  44. package/dist/agent.d.ts.map +1 -1
  45. package/dist/agent.js +677 -552
  46. package/dist/agent.js.map +1 -1
  47. package/dist/commands/auto-reply.d.ts +16 -0
  48. package/dist/commands/auto-reply.d.ts.map +1 -0
  49. package/dist/commands/auto-reply.js +72 -0
  50. package/dist/commands/auto-reply.js.map +1 -0
  51. package/dist/commands/index.d.ts +5 -0
  52. package/dist/commands/index.d.ts.map +1 -0
  53. package/dist/commands/index.js +18 -0
  54. package/dist/commands/index.js.map +1 -0
  55. package/dist/commands/login.d.ts +5 -0
  56. package/dist/commands/login.d.ts.map +1 -0
  57. package/dist/commands/login.js +91 -0
  58. package/dist/commands/login.js.map +1 -0
  59. package/dist/commands/model.d.ts +14 -0
  60. package/dist/commands/model.d.ts.map +1 -0
  61. package/dist/commands/model.js +112 -0
  62. package/dist/commands/model.js.map +1 -0
  63. package/dist/commands/new.d.ts +9 -0
  64. package/dist/commands/new.d.ts.map +1 -0
  65. package/dist/commands/new.js +28 -0
  66. package/dist/commands/new.js.map +1 -0
  67. package/dist/commands/registry.d.ts +4 -0
  68. package/dist/commands/registry.d.ts.map +1 -0
  69. package/dist/commands/registry.js +9 -0
  70. package/dist/commands/registry.js.map +1 -0
  71. package/dist/commands/sandbox.d.ts +10 -0
  72. package/dist/commands/sandbox.d.ts.map +1 -0
  73. package/dist/commands/sandbox.js +88 -0
  74. package/dist/commands/sandbox.js.map +1 -0
  75. package/dist/commands/session-view.d.ts +5 -0
  76. package/dist/commands/session-view.d.ts.map +1 -0
  77. package/dist/commands/session-view.js +62 -0
  78. package/dist/commands/session-view.js.map +1 -0
  79. package/dist/commands/types.d.ts +41 -0
  80. package/dist/commands/types.d.ts.map +1 -0
  81. package/dist/commands/types.js +2 -0
  82. package/dist/commands/types.js.map +1 -0
  83. package/dist/commands/utils.d.ts +8 -0
  84. package/dist/commands/utils.d.ts.map +1 -0
  85. package/dist/commands/utils.js +14 -0
  86. package/dist/commands/utils.js.map +1 -0
  87. package/dist/config.d.ts +46 -8
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +306 -67
  90. package/dist/config.js.map +1 -1
  91. package/dist/context.d.ts +10 -42
  92. package/dist/context.d.ts.map +1 -1
  93. package/dist/context.js +14 -127
  94. package/dist/context.js.map +1 -1
  95. package/dist/events.d.ts +2 -0
  96. package/dist/events.d.ts.map +1 -1
  97. package/dist/events.js +148 -67
  98. package/dist/events.js.map +1 -1
  99. package/dist/execution-resolver.d.ts +12 -7
  100. package/dist/execution-resolver.d.ts.map +1 -1
  101. package/dist/execution-resolver.js +150 -21
  102. package/dist/execution-resolver.js.map +1 -1
  103. package/dist/file-guards.d.ts +9 -0
  104. package/dist/file-guards.d.ts.map +1 -0
  105. package/dist/file-guards.js +56 -0
  106. package/dist/file-guards.js.map +1 -0
  107. package/dist/fs-atomic.d.ts +10 -0
  108. package/dist/fs-atomic.d.ts.map +1 -0
  109. package/dist/fs-atomic.js +45 -0
  110. package/dist/fs-atomic.js.map +1 -0
  111. package/dist/index.d.ts +7 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +4 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/instrument.d.ts.map +1 -1
  116. package/dist/instrument.js +2 -3
  117. package/dist/instrument.js.map +1 -1
  118. package/dist/log.d.ts +1 -12
  119. package/dist/log.d.ts.map +1 -1
  120. package/dist/log.js +12 -143
  121. package/dist/log.js.map +1 -1
  122. package/dist/{login.d.ts → login/index.d.ts} +16 -3
  123. package/dist/login/index.d.ts.map +1 -0
  124. package/dist/{login.js → login/index.js} +94 -17
  125. package/dist/login/index.js.map +1 -0
  126. package/dist/{link-server.d.ts → login/portal.d.ts} +6 -4
  127. package/dist/login/portal.d.ts.map +1 -0
  128. package/dist/login/portal.js +1544 -0
  129. package/dist/login/portal.js.map +1 -0
  130. package/dist/login/session.d.ts +26 -0
  131. package/dist/login/session.d.ts.map +1 -0
  132. package/dist/{link-token.js → login/session.js} +10 -22
  133. package/dist/login/session.js.map +1 -0
  134. package/dist/main.d.ts.map +1 -1
  135. package/dist/main.js +138 -352
  136. package/dist/main.js.map +1 -1
  137. package/dist/provisioner.d.ts +42 -11
  138. package/dist/provisioner.d.ts.map +1 -1
  139. package/dist/provisioner.js +273 -64
  140. package/dist/provisioner.js.map +1 -1
  141. package/dist/runtime/conversation-orchestrator.d.ts +40 -0
  142. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  143. package/dist/runtime/conversation-orchestrator.js +183 -0
  144. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  145. package/dist/runtime/index.d.ts +2 -0
  146. package/dist/runtime/index.d.ts.map +1 -0
  147. package/dist/runtime/index.js +2 -0
  148. package/dist/runtime/index.js.map +1 -0
  149. package/dist/runtime/session-runtime.d.ts +26 -0
  150. package/dist/runtime/session-runtime.d.ts.map +1 -0
  151. package/dist/runtime/session-runtime.js +221 -0
  152. package/dist/runtime/session-runtime.js.map +1 -0
  153. package/dist/sandbox/cloudflare.d.ts +15 -0
  154. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  155. package/dist/sandbox/cloudflare.js +137 -0
  156. package/dist/sandbox/cloudflare.js.map +1 -0
  157. package/dist/sandbox/container.d.ts +2 -1
  158. package/dist/sandbox/container.d.ts.map +1 -1
  159. package/dist/sandbox/container.js +18 -2
  160. package/dist/sandbox/container.js.map +1 -1
  161. package/dist/sandbox/firecracker.d.ts +2 -1
  162. package/dist/sandbox/firecracker.d.ts.map +1 -1
  163. package/dist/sandbox/firecracker.js +6 -0
  164. package/dist/sandbox/firecracker.js.map +1 -1
  165. package/dist/sandbox/host.d.ts +2 -1
  166. package/dist/sandbox/host.d.ts.map +1 -1
  167. package/dist/sandbox/host.js +4 -0
  168. package/dist/sandbox/host.js.map +1 -1
  169. package/dist/sandbox/index.d.ts +6 -4
  170. package/dist/sandbox/index.d.ts.map +1 -1
  171. package/dist/sandbox/index.js +9 -6
  172. package/dist/sandbox/index.js.map +1 -1
  173. package/dist/sandbox/path-context.d.ts +4 -0
  174. package/dist/sandbox/path-context.d.ts.map +1 -0
  175. package/dist/sandbox/path-context.js +20 -0
  176. package/dist/sandbox/path-context.js.map +1 -0
  177. package/dist/sandbox/types.d.ts +17 -1
  178. package/dist/sandbox/types.d.ts.map +1 -1
  179. package/dist/sandbox/types.js.map +1 -1
  180. package/dist/sentry.d.ts +20 -1
  181. package/dist/sentry.d.ts.map +1 -1
  182. package/dist/sentry.js +58 -8
  183. package/dist/sentry.js.map +1 -1
  184. package/dist/session-policy.d.ts +13 -0
  185. package/dist/session-policy.d.ts.map +1 -0
  186. package/dist/session-policy.js +23 -0
  187. package/dist/session-policy.js.map +1 -0
  188. package/dist/session-store.d.ts +33 -2
  189. package/dist/session-store.d.ts.map +1 -1
  190. package/dist/session-store.js +179 -13
  191. package/dist/session-store.js.map +1 -1
  192. package/dist/session-view/command.d.ts +5 -0
  193. package/dist/session-view/command.d.ts.map +1 -0
  194. package/dist/session-view/command.js +11 -0
  195. package/dist/session-view/command.js.map +1 -0
  196. package/dist/session-view/portal.d.ts +16 -0
  197. package/dist/session-view/portal.d.ts.map +1 -0
  198. package/dist/session-view/portal.js +1822 -0
  199. package/dist/session-view/portal.js.map +1 -0
  200. package/dist/session-view/service.d.ts +34 -0
  201. package/dist/session-view/service.d.ts.map +1 -0
  202. package/dist/session-view/service.js +427 -0
  203. package/dist/session-view/service.js.map +1 -0
  204. package/dist/session-view/store.d.ts +18 -0
  205. package/dist/session-view/store.d.ts.map +1 -0
  206. package/dist/session-view/store.js +36 -0
  207. package/dist/session-view/store.js.map +1 -0
  208. package/dist/store.d.ts +3 -6
  209. package/dist/store.d.ts.map +1 -1
  210. package/dist/store.js +22 -48
  211. package/dist/store.js.map +1 -1
  212. package/dist/tool-diagnostics.d.ts +2 -0
  213. package/dist/tool-diagnostics.d.ts.map +1 -0
  214. package/dist/tool-diagnostics.js +7 -0
  215. package/dist/tool-diagnostics.js.map +1 -0
  216. package/dist/tools/bash.d.ts +2 -2
  217. package/dist/tools/bash.d.ts.map +1 -1
  218. package/dist/tools/bash.js.map +1 -1
  219. package/dist/tools/edit.d.ts +2 -2
  220. package/dist/tools/edit.d.ts.map +1 -1
  221. package/dist/tools/edit.js.map +1 -1
  222. package/dist/tools/event.d.ts +42 -2
  223. package/dist/tools/event.d.ts.map +1 -1
  224. package/dist/tools/event.js +43 -9
  225. package/dist/tools/event.js.map +1 -1
  226. package/dist/tools/index.d.ts +2 -2
  227. package/dist/tools/index.d.ts.map +1 -1
  228. package/dist/tools/index.js +2 -2
  229. package/dist/tools/index.js.map +1 -1
  230. package/dist/tools/read.d.ts +2 -2
  231. package/dist/tools/read.d.ts.map +1 -1
  232. package/dist/tools/read.js.map +1 -1
  233. package/dist/tools/write.d.ts +2 -2
  234. package/dist/tools/write.d.ts.map +1 -1
  235. package/dist/tools/write.js.map +1 -1
  236. package/dist/trigger.d.ts +31 -0
  237. package/dist/trigger.d.ts.map +1 -0
  238. package/dist/trigger.js +98 -0
  239. package/dist/trigger.js.map +1 -0
  240. package/dist/vault-routing.d.ts +2 -7
  241. package/dist/vault-routing.d.ts.map +1 -1
  242. package/dist/vault-routing.js +6 -42
  243. package/dist/vault-routing.js.map +1 -1
  244. package/dist/vault.d.ts +22 -56
  245. package/dist/vault.d.ts.map +1 -1
  246. package/dist/vault.js +155 -263
  247. package/dist/vault.js.map +1 -1
  248. package/package.json +11 -11
  249. package/dist/bindings.d.ts +0 -44
  250. package/dist/bindings.d.ts.map +0 -1
  251. package/dist/bindings.js +0 -74
  252. package/dist/bindings.js.map +0 -1
  253. package/dist/link-server.d.ts.map +0 -1
  254. package/dist/link-server.js +0 -899
  255. package/dist/link-server.js.map +0 -1
  256. package/dist/link-token.d.ts +0 -32
  257. package/dist/link-token.d.ts.map +0 -1
  258. package/dist/link-token.js.map +0 -1
  259. package/dist/login.d.ts.map +0 -1
  260. package/dist/login.js.map +0 -1
  261. package/dist/sandbox.d.ts +0 -2
  262. package/dist/sandbox.d.ts.map +0 -1
  263. package/dist/sandbox.js +0 -2
  264. package/dist/sandbox.js.map +0 -1
package/dist/main.js CHANGED
@@ -1,34 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import "./instrument.js";
3
3
  import { join, resolve } from "path";
4
- import { mkdirSync, readFileSync, statSync } from "fs";
4
+ import { mkdirSync, statSync, writeFileSync } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { fileURLToPath } from "url";
7
7
  import { dirname, join as pathJoin } from "path";
8
8
  import { DiscordBot } from "./adapters/discord/index.js";
9
9
  import { TelegramBot } from "./adapters/telegram/index.js";
10
10
  import { SlackBot as SlackBotClass } from "./adapters/slack/index.js";
11
- import { createRunner } from "./agent.js";
12
- import { createManagedSessionFile, createManagedSessionFileAtPath, getChannelSessionDir, getThreadSessionFile, } from "./session-store.js";
13
11
  import { downloadChannel } from "./download.js";
14
12
  import { createEventsWatcher } from "./events.js";
15
13
  import * as log from "./log.js";
16
- import { FileUserBindingStore } from "./bindings.js";
17
- import { startLinkServer } from "./link-server.js";
18
- import { parseLoginCommand } from "./login.js";
19
- import { InMemoryLinkTokenStore } from "./link-token.js";
14
+ import { startLinkServer } from "./login/portal.js";
15
+ import { InMemoryLinkTokenStore } from "./login/session.js";
16
+ import { InMemorySessionViewTokenStore } from "./session-view/store.js";
20
17
  import { DockerContainerManager } from "./provisioner.js";
21
- import { SandboxError, parseSandboxArg, validateSandbox } from "./sandbox.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/index.js";
22
21
  import { FileVaultManager } from "./vault.js";
23
- import { createManagedVaultEntry, ensureSandboxVaultEntry, resolveActorVaultKey, } from "./vault-routing.js";
24
- import { addLifecycleBreadcrumb, applyRunScope } from "./sentry.js";
22
+ import { createSessionRuntime } from "./runtime/index.js";
25
23
  import { ChannelStore } from "./store.js";
26
- import { formatNothingRunning, formatStopped, formatStopping } from "./ui-copy.js";
27
24
  import * as Sentry from "@sentry/node";
28
- // ============================================================================
29
- // Config
30
- // ============================================================================
31
- // Get version from package.json
32
25
  function getVersion() {
33
26
  // Try to find package.json in the dist directory or parent
34
27
  const possiblePaths = [
@@ -37,25 +30,20 @@ function getVersion() {
37
30
  pathJoin(process.cwd(), "package.json"),
38
31
  ];
39
32
  for (const pkgPath of possiblePaths) {
40
- try {
41
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
42
- if (pkg.version)
43
- return pkg.version;
44
- }
45
- catch {
46
- // Continue to next path
47
- }
33
+ const pkg = readJsonFileIfExists(pkgPath, (value) => isRecord(value), () => "Ignoring package.json while resolving version");
34
+ if (typeof pkg?.version === "string" && pkg.version)
35
+ return pkg.version;
48
36
  }
49
37
  return "unknown";
50
38
  }
51
- const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
52
- const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
53
- const MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;
54
- const MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;
55
- const MOM_LINK_URL = process.env.MOM_LINK_URL;
56
- const MOM_LINK_PORT = process.env.MOM_LINK_PORT
57
- ? parseInt(process.env.MOM_LINK_PORT, 10)
58
- : MOM_LINK_URL
39
+ const MAMA_SLACK_APP_TOKEN = process.env.MAMA_SLACK_APP_TOKEN;
40
+ const MAMA_SLACK_BOT_TOKEN = process.env.MAMA_SLACK_BOT_TOKEN;
41
+ const MAMA_TELEGRAM_BOT_TOKEN = process.env.MAMA_TELEGRAM_BOT_TOKEN;
42
+ const MAMA_DISCORD_BOT_TOKEN = process.env.MAMA_DISCORD_BOT_TOKEN;
43
+ const MAMA_LINK_URL = process.env.MAMA_LINK_URL;
44
+ const MAMA_LINK_PORT = process.env.MAMA_LINK_PORT
45
+ ? parseInt(process.env.MAMA_LINK_PORT, 10)
46
+ : MAMA_LINK_URL
59
47
  ? 8181
60
48
  : undefined;
61
49
  function parseArgs() {
@@ -64,12 +52,16 @@ function parseArgs() {
64
52
  let workingDir;
65
53
  let stateDirArg;
66
54
  let downloadChannelId;
55
+ let showOnboard = false;
67
56
  let showVersion = false;
68
57
  for (let i = 0; i < args.length; i++) {
69
58
  const arg = args[i];
70
59
  if (arg === "--version" || arg === "-v" || arg === "-V") {
71
60
  showVersion = true;
72
61
  }
62
+ else if (arg === "--onboard") {
63
+ showOnboard = true;
64
+ }
73
65
  else if (arg.startsWith("--sandbox=")) {
74
66
  sandbox = parseSandboxArg(arg.slice("--sandbox=".length));
75
67
  }
@@ -97,6 +89,7 @@ function parseArgs() {
97
89
  stateDir: stateDirArg ? resolve(stateDirArg) : undefined,
98
90
  sandbox,
99
91
  downloadChannel: downloadChannelId,
92
+ showOnboard,
100
93
  showVersion,
101
94
  };
102
95
  }
@@ -139,7 +132,21 @@ function handleStartupError(error) {
139
132
  }
140
133
  process.exit(1);
141
134
  }
142
- throw error;
135
+ if (error instanceof MissingGlobalSettingsError) {
136
+ console.error(`Missing global settings: ${error.settingsPath}`);
137
+ console.error("");
138
+ console.error("Run onboarding to create it:");
139
+ console.error(` mama --onboard --state-dir ${stateDir}`);
140
+ console.error("");
141
+ console.error("Then review the generated settings.json and start mama again.");
142
+ process.exit(1);
143
+ }
144
+ if (error instanceof Error) {
145
+ console.error(`Error: ${error.message}`);
146
+ process.exit(1);
147
+ }
148
+ console.error(String(error));
149
+ process.exit(1);
143
150
  }
144
151
  let parsedArgs;
145
152
  try {
@@ -153,18 +160,35 @@ if (parsedArgs.showVersion) {
153
160
  console.log(getVersion());
154
161
  process.exit(0);
155
162
  }
163
+ // Handle --onboard mode
164
+ if (parsedArgs.showOnboard) {
165
+ const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
166
+ process.env.MAMA_STATE_DIR = stateDir;
167
+ ensureSecureStateDir(stateDir);
168
+ try {
169
+ const settingsPath = createGlobalSettingsFile(stateDir);
170
+ console.log(`Created global settings at ${settingsPath}`);
171
+ console.log("Review the file, then start mama with your working directory.");
172
+ process.exit(0);
173
+ }
174
+ catch (err) {
175
+ console.error(err instanceof Error ? err.message : String(err));
176
+ process.exit(1);
177
+ }
178
+ }
156
179
  // Handle --download mode (Slack only)
157
180
  if (parsedArgs.downloadChannel) {
158
- if (!MOM_SLACK_BOT_TOKEN) {
159
- console.error("Missing env: MOM_SLACK_BOT_TOKEN");
181
+ if (!MAMA_SLACK_BOT_TOKEN) {
182
+ console.error("Missing env: MAMA_SLACK_BOT_TOKEN");
160
183
  process.exit(1);
161
184
  }
162
- await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);
185
+ await downloadChannel(parsedArgs.downloadChannel, MAMA_SLACK_BOT_TOKEN);
163
186
  process.exit(0);
164
187
  }
165
188
  // Normal bot mode - require working dir
166
189
  if (!parsedArgs.workingDir) {
167
- console.error("Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>");
190
+ console.error("Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>|cloudflare:<sandbox-id>] <working-directory>");
191
+ console.error(" mama --onboard [--state-dir=<dir>]");
168
192
  console.error(" mama --download <channel-id>");
169
193
  process.exit(1);
170
194
  }
@@ -173,14 +197,14 @@ const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
173
197
  process.env.MAMA_STATE_DIR = stateDir;
174
198
  ensureSecureStateDir(stateDir);
175
199
  // Validate platform tokens
176
- const hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);
177
- const hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;
178
- const hasDiscord = !!MOM_DISCORD_BOT_TOKEN;
200
+ const hasSlack = !!(MAMA_SLACK_APP_TOKEN && MAMA_SLACK_BOT_TOKEN);
201
+ const hasTelegram = !!MAMA_TELEGRAM_BOT_TOKEN;
202
+ const hasDiscord = !!MAMA_DISCORD_BOT_TOKEN;
179
203
  if (!hasSlack && !hasTelegram && !hasDiscord) {
180
204
  console.error("No platform tokens found. Set one of:\n" +
181
- " Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\n" +
182
- " Telegram: MOM_TELEGRAM_BOT_TOKEN\n" +
183
- " Discord: MOM_DISCORD_BOT_TOKEN");
205
+ " Slack: MAMA_SLACK_APP_TOKEN + MAMA_SLACK_BOT_TOKEN\n" +
206
+ " Telegram: MAMA_TELEGRAM_BOT_TOKEN\n" +
207
+ " Discord: MAMA_DISCORD_BOT_TOKEN");
184
208
  process.exit(1);
185
209
  }
186
210
  try {
@@ -193,84 +217,52 @@ const vaultManager = new FileVaultManager(stateDir);
193
217
  if (vaultManager.isEnabled()) {
194
218
  console.log(sandbox.type === "container"
195
219
  ? " Vault system enabled. Container vault active."
196
- : sandbox.type === "image" || sandbox.type === "firecracker"
197
- ? " Vault system enabled. Per-user credential routing active."
220
+ : sandbox.type === "image" || sandbox.type === "firecracker" || sandbox.type === "cloudflare"
221
+ ? " Vault system enabled. Conversation-scoped credential routing active."
198
222
  : " Vault system enabled. Host mode will not inject vault env.");
199
223
  }
200
- const bindingStore = new FileUserBindingStore(stateDir);
201
- if (bindingStore.isEnabled()) {
202
- console.log(sandbox.type === "container"
203
- ? " Binding store enabled. Container mode uses the container vault."
204
- : sandbox.type === "image" || sandbox.type === "firecracker"
205
- ? " Binding store enabled. Platform user → vault routing active."
206
- : " Binding store enabled. Host mode will not inject vault env.");
207
- }
208
- const provisioner = sandbox.type === "image" ? new DockerContainerManager(sandbox.image, workingDir) : undefined;
209
- const linkTokenStore = new InMemoryLinkTokenStore();
210
- setInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();
211
- function normalizeLoginBaseUrl() {
212
- if (MOM_LINK_URL) {
213
- return MOM_LINK_URL.replace(/\/+$/, "");
214
- }
215
- if (MOM_LINK_PORT) {
216
- return `http://localhost:${MOM_LINK_PORT}`;
217
- }
218
- return undefined;
219
- }
220
- function isPrivateConversation(event) {
221
- return (event.conversationKind === "direct" ||
222
- event.type === "dm" ||
223
- event.sessionKey === event.conversationId);
224
- }
225
- function ensureLoginVault(platform, platformUserId) {
226
- const vaultId = resolveActorVaultKey(sandbox, vaultManager, bindingStore, platform, platformUserId);
227
- ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);
228
- if (sandbox.type !== "container" && sandbox.type !== "image") {
229
- vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));
230
- }
231
- return vaultId;
232
- }
233
- async function replyWithContext(responseCtx, text) {
234
- await responseCtx.setTyping(false);
235
- await responseCtx.setWorking(false);
236
- await responseCtx.respond(text);
237
- }
238
- async function handleLoginCommand(platform, platformUserId, conversationId, responseCtx, commandText, privateConversation) {
239
- const parsed = parseLoginCommand(commandText);
240
- if (!parsed)
241
- return false;
242
- if (!privateConversation) {
243
- await replyWithContext(responseCtx, "為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。");
244
- return true;
224
+ const startupConfig = (() => {
225
+ try {
226
+ return loadAgentConfig();
245
227
  }
246
- const baseUrl = normalizeLoginBaseUrl();
247
- if (!baseUrl) {
248
- await replyWithContext(responseCtx, "Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
249
- return true;
228
+ catch (error) {
229
+ handleStartupError(error);
250
230
  }
251
- let vaultId;
231
+ })();
232
+ const sandboxLimits = startupConfig.sandboxCpus || startupConfig.sandboxMemory
233
+ ? { cpus: startupConfig.sandboxCpus, memory: startupConfig.sandboxMemory }
234
+ : undefined;
235
+ const sandboxBoostLimits = startupConfig.sandboxBoostCpus || startupConfig.sandboxBoostMemory
236
+ ? { cpus: startupConfig.sandboxBoostCpus, memory: startupConfig.sandboxBoostMemory }
237
+ : undefined;
238
+ const provisioner = sandbox.type === "image"
239
+ ? new DockerContainerManager(sandbox.image, {
240
+ limits: sandboxLimits,
241
+ boostLimits: sandboxBoostLimits,
242
+ })
243
+ : undefined;
244
+ if (sandbox.type === "image") {
245
+ ensureDirExists(join(workingDir, "skills"));
246
+ ensureDirExists(join(workingDir, "events"));
252
247
  try {
253
- vaultId = ensureLoginVault(platform, platformUserId);
248
+ writeFileSync(join(workingDir, "MEMORY.md"), "", { flag: "wx" });
254
249
  }
255
- catch (error) {
256
- log.logWarning(`[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`, error instanceof Error ? error.message : String(error));
257
- await replyWithContext(responseCtx, "Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。");
258
- return true;
250
+ catch (err) {
251
+ if (err.code !== "EEXIST")
252
+ throw err;
259
253
  }
260
- const token = linkTokenStore.create(platform, platformUserId, conversationId, vaultId, "");
261
- const vaultLabel = sandbox.type === "container" ? `container vault (${vaultId})` : "your vault";
262
- await replyWithContext(responseCtx, `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\n${baseUrl}/link?token=${token.token}`);
263
- return true;
264
254
  }
265
- const conversationStates = new Map();
266
- /** Track in-flight runs for graceful shutdown */
267
- const inFlightRuns = new Set();
268
- /** Flag to stop accepting new events during shutdown */
269
- let isShuttingDown = false;
270
- /** Maximum number of cached sessions */
271
- const MAX_SESSIONS = 500;
272
- /** Idle timeout before a non-running session can be evicted (1 hour) */
273
- const IDLE_TIMEOUT_MS = 3600000;
255
+ const linkTokenStore = new InMemoryLinkTokenStore();
256
+ const sessionViewTokenStore = new InMemorySessionViewTokenStore();
257
+ setInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();
258
+ setInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();
259
+ function portalBaseUrl() {
260
+ if (MAMA_LINK_URL)
261
+ return MAMA_LINK_URL.replace(/\/+$/, "");
262
+ if (MAMA_LINK_PORT)
263
+ return `http://localhost:${MAMA_LINK_PORT}`;
264
+ return undefined;
265
+ }
274
266
  /** Idle timeout for managed image containers (10 minutes) */
275
267
  const IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
276
268
  if (provisioner) {
@@ -278,241 +270,37 @@ if (provisioner) {
278
270
  await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);
279
271
  setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();
280
272
  }
281
- async function getState(conversationId, sessionKey) {
282
- const key = sessionKey ?? conversationId;
283
- let state = conversationStates.get(key);
284
- if (!state) {
285
- const conversationDir = join(workingDir, conversationId);
286
- state = {
287
- running: false,
288
- runner: await createRunner(sandbox, key, conversationId, conversationDir, workingDir, vaultManager, bindingStore, provisioner),
289
- stopRequested: false,
290
- lastAccessedAt: Date.now(),
291
- };
292
- conversationStates.set(key, state);
293
- }
294
- else {
295
- state.lastAccessedAt = Date.now();
296
- }
297
- return state;
298
- }
299
- /**
300
- * Evict idle sessions from conversationStates to bound memory usage.
301
- * Called after each handleEvent completes.
302
- */
303
- function evictIdleSessions() {
304
- const now = Date.now();
305
- for (const [key, state] of conversationStates) {
306
- if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
307
- conversationStates.delete(key);
308
- }
309
- }
310
- if (conversationStates.size > MAX_SESSIONS) {
311
- const idleSessions = [];
312
- for (const [key, state] of conversationStates) {
313
- if (!state.running) {
314
- idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });
315
- }
316
- }
317
- idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
318
- const toEvict = conversationStates.size - MAX_SESSIONS;
319
- for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
320
- conversationStates.delete(idleSessions[i].key);
321
- }
322
- }
323
- }
324
- // ============================================================================
325
- // Handler
326
- // ============================================================================
327
- const handler = {
328
- isRunning(sessionKey) {
329
- const state = conversationStates.get(sessionKey);
330
- return !!state?.running;
331
- },
332
- getRunningSessions() {
333
- const sessions = [];
334
- for (const [sessionKey, state] of conversationStates) {
335
- if (state.running && state.startedAt) {
336
- // Get current step from runner
337
- const currentStep = state.runner.getCurrentStep();
338
- sessions.push({
339
- sessionKey,
340
- startedAt: state.startedAt,
341
- lastActivityAt: state.lastActivityAt,
342
- currentTool: currentStep?.label || currentStep?.toolName,
343
- });
344
- }
345
- }
346
- return sessions;
347
- },
348
- async handleStop(sessionKey, conversationId, bot) {
349
- const state = conversationStates.get(sessionKey);
350
- if (state?.running) {
351
- state.stopRequested = true;
352
- state.runner.abort();
353
- const ts = await bot.postMessage(conversationId, formatStopping(bot));
354
- state.stopMessageTs = ts;
355
- }
356
- else {
357
- await bot.postMessage(conversationId, formatNothingRunning(bot));
358
- }
359
- },
360
- forceStop(sessionKey) {
361
- const state = conversationStates.get(sessionKey);
362
- if (state?.running) {
363
- log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);
364
- state.stopRequested = true;
365
- state.runner.abort();
366
- state.running = false;
367
- }
368
- },
369
- async handleNew(sessionKey, conversationId, bot) {
370
- const state = conversationStates.get(sessionKey);
371
- if (state?.running) {
372
- state.stopRequested = true;
373
- state.runner.abort();
374
- }
375
- // Conversation sessions rotate via current pointer. Thread sessions reset in place.
376
- const conversationDir = join(workingDir, conversationId);
377
- if (sessionKey.includes(":")) {
378
- createManagedSessionFileAtPath(getThreadSessionFile(conversationDir, sessionKey), conversationDir);
379
- }
380
- else {
381
- createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);
382
- }
383
- // Remove from in-memory cache
384
- conversationStates.delete(sessionKey);
385
- log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);
386
- await bot.postMessage(conversationId, "Conversation reset. Send a new message to start fresh.");
387
- },
388
- async handleEvent(event, bot, adapters, _isEvent) {
389
- const conversationId = event.conversationId;
390
- // Don't accept new events during shutdown
391
- if (isShuttingDown) {
392
- log.logInfo(`[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
393
- return;
394
- }
395
- const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;
396
- const handledLogin = await handleLoginCommand(adapters.platform.name, event.user, conversationId, adapters.responseCtx, event.text, isPrivateConversation(event));
397
- if (handledLogin)
398
- return;
399
- const state = await getState(conversationId, sessionKey);
400
- // Start run
401
- state.running = true;
402
- state.stopRequested = false;
403
- state.startedAt = Date.now();
404
- state.lastActivityAt = Date.now();
405
- log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);
406
- // Wrap in-flight run tracking
407
- Sentry.metrics.count("agent.run.started", 1, {
408
- attributes: { channel: conversationId },
409
- });
410
- Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size + 1);
411
- const runPromise = Sentry.startSpan({ name: "agent.run", op: "agent", attributes: { conversationId, sessionKey } }, async () => {
412
- return Sentry.withScope(async (scope) => {
413
- const { message, responseCtx, platform } = adapters;
414
- applyRunScope(scope, {
415
- conversationId,
416
- sessionKey,
417
- messageId: message.id,
418
- platform: platform.name,
419
- userId: message.userId,
420
- userName: message.userName,
421
- threadTs: message.threadTs,
422
- isEvent: _isEvent,
423
- });
424
- addLifecycleBreadcrumb("agent.run.started", {
425
- channel_id: conversationId,
426
- platform: platform.name,
427
- has_attachments: (message.attachments?.length ?? 0) > 0,
428
- });
429
- try {
430
- await responseCtx.setTyping(true);
431
- await responseCtx.setWorking(true);
432
- const result = await state.runner.run(message, responseCtx, platform);
433
- await responseCtx.setWorking(false);
434
- const durationMs = Date.now() - state.startedAt;
435
- Sentry.metrics.distribution("agent.run.duration", durationMs, {
436
- unit: "millisecond",
437
- attributes: {
438
- channel: conversationId,
439
- platform: platform.name,
440
- stop_reason: result.stopReason,
441
- },
442
- });
443
- Sentry.metrics.count("agent.run.completed", 1, {
444
- attributes: {
445
- channel: conversationId,
446
- platform: platform.name,
447
- stop_reason: result.stopReason,
448
- },
449
- });
450
- addLifecycleBreadcrumb("agent.run.completed", {
451
- channel_id: conversationId,
452
- platform: platform.name,
453
- stop_reason: result.stopReason,
454
- duration_ms: durationMs,
455
- });
456
- if (result.stopReason === "aborted" && state.stopRequested) {
457
- if (state.stopMessageTs) {
458
- await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));
459
- state.stopMessageTs = undefined;
460
- }
461
- else {
462
- await bot.postMessage(conversationId, formatStopped(bot));
463
- }
464
- }
465
- }
466
- catch (err) {
467
- scope.setContext("agent_run_error", {
468
- conversationId,
469
- sessionKey,
470
- platform: adapters.platform.name,
471
- messageId: adapters.message.id,
472
- threadTs: adapters.message.threadTs,
473
- });
474
- Sentry.captureException(err);
475
- Sentry.metrics.count("agent.run.errors", 1, {
476
- attributes: { channel: conversationId, platform: adapters.platform.name },
477
- });
478
- log.logWarning(`[${conversationId}] Run error`, err instanceof Error ? err.message : String(err));
479
- }
480
- finally {
481
- state.running = false;
482
- state.lastAccessedAt = Date.now();
483
- Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size - 1);
484
- evictIdleSessions();
485
- }
486
- });
487
- });
488
- inFlightRuns.add(runPromise);
489
- try {
490
- await runPromise;
491
- }
492
- finally {
493
- inFlightRuns.delete(runPromise);
494
- }
495
- },
496
- };
497
- // ============================================================================
498
- // Start
499
- // ============================================================================
273
+ const handler = createSessionRuntime({
274
+ workingDir,
275
+ sandbox,
276
+ vaultManager,
277
+ provisioner,
278
+ linkTokenStore,
279
+ sessionViewTokenStore,
280
+ portalBaseUrl: portalBaseUrl(),
281
+ });
500
282
  const sandboxDesc = sandbox.type === "host"
501
283
  ? "host"
502
284
  : sandbox.type === "container"
503
285
  ? `container:${sandbox.container}`
504
286
  : sandbox.type === "image"
505
287
  ? `image:${sandbox.image}`
506
- : `firecracker:${sandbox.vmId}`;
288
+ : sandbox.type === "firecracker"
289
+ ? `firecracker:${sandbox.vmId}`
290
+ : `cloudflare:${sandbox.sandboxId}`;
507
291
  log.logStartup(workingDir, sandboxDesc);
508
- // Create platform bots
509
292
  const bots = [];
510
293
  const botsByPlatform = {};
511
294
  if (hasSlack) {
512
- const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN });
295
+ const slackBotToken = MAMA_SLACK_BOT_TOKEN;
296
+ const slackAppToken = MAMA_SLACK_APP_TOKEN;
297
+ if (!slackBotToken || !slackAppToken) {
298
+ throw new Error("Slack startup requires both MAMA_SLACK_APP_TOKEN and MAMA_SLACK_BOT_TOKEN");
299
+ }
300
+ const sharedStore = new ChannelStore({ workingDir, botToken: slackBotToken });
513
301
  const slackBot = new SlackBotClass(handler, {
514
- appToken: MOM_SLACK_APP_TOKEN,
515
- botToken: MOM_SLACK_BOT_TOKEN,
302
+ appToken: slackAppToken,
303
+ botToken: slackBotToken,
516
304
  workingDir,
517
305
  store: sharedStore,
518
306
  });
@@ -521,8 +309,12 @@ if (hasSlack) {
521
309
  log.logInfo("Platform: Slack");
522
310
  }
523
311
  if (hasTelegram) {
312
+ const telegramToken = MAMA_TELEGRAM_BOT_TOKEN;
313
+ if (!telegramToken) {
314
+ throw new Error("Telegram startup requires MAMA_TELEGRAM_BOT_TOKEN");
315
+ }
524
316
  const telegramBot = new TelegramBot(handler, {
525
- token: MOM_TELEGRAM_BOT_TOKEN,
317
+ token: telegramToken,
526
318
  workingDir,
527
319
  });
528
320
  bots.push(telegramBot);
@@ -530,20 +322,24 @@ if (hasTelegram) {
530
322
  log.logInfo("Platform: Telegram");
531
323
  }
532
324
  if (hasDiscord) {
325
+ const discordToken = MAMA_DISCORD_BOT_TOKEN;
326
+ if (!discordToken) {
327
+ throw new Error("Discord startup requires MAMA_DISCORD_BOT_TOKEN");
328
+ }
533
329
  const discordBot = new DiscordBot(handler, {
534
- token: MOM_DISCORD_BOT_TOKEN,
330
+ token: discordToken,
535
331
  workingDir,
536
332
  });
537
333
  bots.push(discordBot);
538
334
  botsByPlatform.discord = discordBot;
539
335
  log.logInfo("Platform: Discord");
540
336
  }
541
- if (MOM_LINK_PORT) {
542
- startLinkServer(MOM_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
337
+ if (MAMA_LINK_PORT) {
338
+ startLinkServer(MAMA_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
543
339
  const bot = botsByPlatform[platform];
544
340
  if (bot)
545
341
  await bot.postMessage(conversationId, message);
546
- });
342
+ }, sessionViewTokenStore, { handler, botsByPlatform });
547
343
  }
548
344
  // Start events watcher with explicit platform routing
549
345
  const eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);
@@ -554,17 +350,7 @@ if (slackBot) {
554
350
  eventsWatcher.start();
555
351
  // Handle shutdown
556
352
  async function shutdown() {
557
- if (isShuttingDown)
558
- return;
559
- isShuttingDown = true;
560
- log.logInfo("Shutting down gracefully...");
561
- const timeout = Date.now() + 30000;
562
- while (inFlightRuns.size > 0 && Date.now() < timeout) {
563
- await new Promise((resolve) => setTimeout(resolve, 500));
564
- }
565
- if (inFlightRuns.size > 0) {
566
- log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);
567
- }
353
+ await handler.shutdown();
568
354
  eventsWatcher.stop();
569
355
  await Sentry.close(5000);
570
356
  process.exit(0);