@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -27
- package/dist/adapter.d.ts +9 -5
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +9 -6
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +16 -13
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +10 -2
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +196 -32
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +24 -17
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +2 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +109 -29
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +8 -43
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +4 -9
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +141 -92
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +44 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +74 -0
- package/dist/bindings.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +53 -12
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +7 -7
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +9 -9
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +14 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +45 -10
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +20 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +49 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +2 -1
- package/dist/instrument.js.map +1 -1
- package/dist/link-server.d.ts +17 -0
- package/dist/link-server.d.ts.map +1 -0
- package/dist/link-server.js +899 -0
- package/dist/link-server.js.map +1 -0
- package/dist/link-token.d.ts +32 -0
- package/dist/link-token.d.ts.map +1 -0
- package/dist/link-token.js +68 -0
- package/dist/link-token.js.map +1 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +7 -7
- package/dist/log.js.map +1 -1
- package/dist/login.d.ts +29 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +164 -0
- package/dist/login.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +226 -55
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +52 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +291 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/sandbox/container.d.ts +15 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +122 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +16 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +206 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +10 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +85 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +20 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +51 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +51 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +4 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +2 -6
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +3 -10
- package/dist/session-store.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +8 -8
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +22 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +104 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +7 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +12 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +36 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +9 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +52 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +106 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +389 -0
- package/dist/vault.js.map +1 -0
- package/package.json +12 -11
package/dist/login.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const DEFAULT_GOOGLE_WORKSPACE_CLI_SCOPES = [
|
|
2
|
+
"https://www.googleapis.com/auth/drive",
|
|
3
|
+
"https://mail.google.com/",
|
|
4
|
+
"https://www.googleapis.com/auth/calendar",
|
|
5
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
6
|
+
"https://www.googleapis.com/auth/documents",
|
|
7
|
+
"https://www.googleapis.com/auth/chat.messages.create",
|
|
8
|
+
];
|
|
9
|
+
// Conservative default: enough for `gh` CLI repo/user/org operations, but
|
|
10
|
+
// without `workflow` (can dispatch CI), `write:packages` (can publish
|
|
11
|
+
// packages), or `project`. Operators who need those can opt in via
|
|
12
|
+
// MOM_GITHUB_OAUTH_SCOPES to keep the blast radius of a compromised agent
|
|
13
|
+
// host explicit and configurable.
|
|
14
|
+
const DEFAULT_GITHUB_OAUTH_SCOPES = ["repo", "read:user", "user:email", "read:org", "gist"];
|
|
15
|
+
function resolveScopesFromEnv(envKey, fallback) {
|
|
16
|
+
const raw = process.env[envKey]?.trim();
|
|
17
|
+
if (!raw)
|
|
18
|
+
return fallback;
|
|
19
|
+
const scopes = raw
|
|
20
|
+
.split(/[\s,]+/)
|
|
21
|
+
.map((scope) => scope.trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
return scopes.length > 0 ? scopes : fallback;
|
|
24
|
+
}
|
|
25
|
+
function resolveGoogleWorkspaceCliScopes() {
|
|
26
|
+
return resolveScopesFromEnv("MOM_GOOGLE_WORKSPACE_CLI_OAUTH_SCOPES", DEFAULT_GOOGLE_WORKSPACE_CLI_SCOPES);
|
|
27
|
+
}
|
|
28
|
+
function resolveGitHubOAuthScopes() {
|
|
29
|
+
return resolveScopesFromEnv("MOM_GITHUB_OAUTH_SCOPES", DEFAULT_GITHUB_OAUTH_SCOPES);
|
|
30
|
+
}
|
|
31
|
+
function getBuiltinOAuthServices() {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
id: "github",
|
|
35
|
+
label: "GitHub",
|
|
36
|
+
aliases: ["github", "github_oauth", "gh_oauth"],
|
|
37
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
38
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
39
|
+
scopes: resolveGitHubOAuthScopes(),
|
|
40
|
+
clientIdEnvKey: "GITHUB_OAUTH_CLIENT_ID",
|
|
41
|
+
clientSecretEnvKey: "GITHUB_OAUTH_CLIENT_SECRET",
|
|
42
|
+
accessTokenEnvKey: "GITHUB_OAUTH_ACCESS_TOKEN",
|
|
43
|
+
additionalAccessTokenEnvKeys: ["GH_TOKEN"],
|
|
44
|
+
refreshTokenEnvKey: "GITHUB_OAUTH_REFRESH_TOKEN",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "google_workspace_cli",
|
|
48
|
+
label: "Google Workspace CLI",
|
|
49
|
+
aliases: ["google_workspace_cli", "gws", "googleworkspace", "google-workspace-cli"],
|
|
50
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
51
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
52
|
+
scopes: resolveGoogleWorkspaceCliScopes(),
|
|
53
|
+
clientIdEnvKey: "GOOGLE_WORKSPACE_CLI_CLIENT_ID",
|
|
54
|
+
clientSecretEnvKey: "GOOGLE_WORKSPACE_CLI_CLIENT_SECRET",
|
|
55
|
+
authorizationParams: {
|
|
56
|
+
access_type: "offline",
|
|
57
|
+
include_granted_scopes: "true",
|
|
58
|
+
prompt: "consent",
|
|
59
|
+
},
|
|
60
|
+
fileOutput: {
|
|
61
|
+
type: "authorized_user",
|
|
62
|
+
relativePath: "gws.json",
|
|
63
|
+
targetPath: "/root/.config/gws/credentials.json",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
export function getOAuthServices() {
|
|
69
|
+
const raw = process.env.MOM_OAUTH_SERVICES_JSON?.trim();
|
|
70
|
+
const builtins = getBuiltinOAuthServices();
|
|
71
|
+
if (!raw)
|
|
72
|
+
return builtins;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
if (!Array.isArray(parsed))
|
|
76
|
+
return builtins;
|
|
77
|
+
const custom = parsed
|
|
78
|
+
.map((entry) => {
|
|
79
|
+
if (!entry || typeof entry !== "object")
|
|
80
|
+
return null;
|
|
81
|
+
const obj = entry;
|
|
82
|
+
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
|
83
|
+
const label = typeof obj.label === "string" ? obj.label.trim() : "";
|
|
84
|
+
const authorizationUrl = typeof obj.authorizationUrl === "string" ? obj.authorizationUrl.trim() : "";
|
|
85
|
+
const tokenUrl = typeof obj.tokenUrl === "string" ? obj.tokenUrl.trim() : "";
|
|
86
|
+
const clientIdEnvKey = typeof obj.clientIdEnvKey === "string" ? obj.clientIdEnvKey.trim() : "";
|
|
87
|
+
const clientSecretEnvKey = typeof obj.clientSecretEnvKey === "string" ? obj.clientSecretEnvKey.trim() : "";
|
|
88
|
+
const accessTokenEnvKey = typeof obj.accessTokenEnvKey === "string" ? obj.accessTokenEnvKey.trim() : undefined;
|
|
89
|
+
if (!id ||
|
|
90
|
+
!label ||
|
|
91
|
+
!authorizationUrl ||
|
|
92
|
+
!tokenUrl ||
|
|
93
|
+
!clientIdEnvKey ||
|
|
94
|
+
!clientSecretEnvKey) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
let fileOutput;
|
|
98
|
+
if (obj.fileOutput && typeof obj.fileOutput === "object") {
|
|
99
|
+
const fileOutputObj = obj.fileOutput;
|
|
100
|
+
const type = typeof fileOutputObj.type === "string" ? fileOutputObj.type.trim() : "";
|
|
101
|
+
const relativePath = typeof fileOutputObj.relativePath === "string" ? fileOutputObj.relativePath.trim() : "";
|
|
102
|
+
const targetPath = typeof fileOutputObj.targetPath === "string"
|
|
103
|
+
? fileOutputObj.targetPath.trim()
|
|
104
|
+
: undefined;
|
|
105
|
+
const envKey = typeof fileOutputObj.envKey === "string" ? fileOutputObj.envKey.trim() : undefined;
|
|
106
|
+
if (type === "authorized_user" && relativePath) {
|
|
107
|
+
fileOutput = { type: "authorized_user", relativePath, targetPath, envKey };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
id: id.toLowerCase(),
|
|
112
|
+
label,
|
|
113
|
+
aliases: Array.isArray(obj.aliases)
|
|
114
|
+
? obj.aliases
|
|
115
|
+
.filter((v) => typeof v === "string")
|
|
116
|
+
.map((v) => v.toLowerCase())
|
|
117
|
+
: [id.toLowerCase()],
|
|
118
|
+
authorizationUrl,
|
|
119
|
+
tokenUrl,
|
|
120
|
+
scopes: Array.isArray(obj.scopes)
|
|
121
|
+
? obj.scopes.filter((v) => typeof v === "string")
|
|
122
|
+
: [],
|
|
123
|
+
clientIdEnvKey,
|
|
124
|
+
clientSecretEnvKey,
|
|
125
|
+
accessTokenEnvKey,
|
|
126
|
+
additionalAccessTokenEnvKeys: Array.isArray(obj.additionalAccessTokenEnvKeys)
|
|
127
|
+
? obj.additionalAccessTokenEnvKeys.filter((v) => typeof v === "string")
|
|
128
|
+
: undefined,
|
|
129
|
+
refreshTokenEnvKey: typeof obj.refreshTokenEnvKey === "string" ? obj.refreshTokenEnvKey.trim() : undefined,
|
|
130
|
+
authorizationParams: obj.authorizationParams && typeof obj.authorizationParams === "object"
|
|
131
|
+
? Object.fromEntries(Object.entries(obj.authorizationParams).filter((entry) => typeof entry[1] === "string"))
|
|
132
|
+
: undefined,
|
|
133
|
+
fileOutput,
|
|
134
|
+
};
|
|
135
|
+
})
|
|
136
|
+
.filter((service) => service !== null);
|
|
137
|
+
const byId = new Map();
|
|
138
|
+
for (const service of builtins)
|
|
139
|
+
byId.set(service.id, service);
|
|
140
|
+
for (const service of custom)
|
|
141
|
+
byId.set(service.id, service);
|
|
142
|
+
return [...byId.values()];
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return builtins;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function resolveOAuthService(input) {
|
|
149
|
+
const normalized = input.trim().toLowerCase();
|
|
150
|
+
if (!normalized)
|
|
151
|
+
return undefined;
|
|
152
|
+
return getOAuthServices().find((service) => service.id === normalized || service.aliases.includes(normalized));
|
|
153
|
+
}
|
|
154
|
+
export function parseLoginCommand(text) {
|
|
155
|
+
const tokens = text.trim().split(/\s+/).filter(Boolean);
|
|
156
|
+
if (tokens.length === 0)
|
|
157
|
+
return null;
|
|
158
|
+
const command = tokens[0].toLowerCase();
|
|
159
|
+
if (command !== "login" && command !== "/login" && command !== "/pi-login") {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return { command: command };
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"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","sourcesContent":["export type LoginCredentialKind = \"api_key\" | \"oauth\";\n\nexport interface OAuthAuthorizedUserFileOutput {\n type: \"authorized_user\";\n relativePath: string;\n targetPath?: string;\n envKey?: string;\n}\n\nexport interface OAuthService {\n id: string;\n label: string;\n aliases: string[];\n authorizationUrl: string;\n tokenUrl: string;\n scopes: string[];\n clientIdEnvKey: string;\n clientSecretEnvKey: string;\n accessTokenEnvKey?: string;\n additionalAccessTokenEnvKeys?: string[];\n refreshTokenEnvKey?: string;\n authorizationParams?: Record<string, string>;\n fileOutput?: OAuthAuthorizedUserFileOutput;\n}\n\nexport interface ParsedLoginCommand {\n command: \"login\" | \"/login\" | \"/pi-login\";\n}\n\nconst DEFAULT_GOOGLE_WORKSPACE_CLI_SCOPES = [\n \"https://www.googleapis.com/auth/drive\",\n \"https://mail.google.com/\",\n \"https://www.googleapis.com/auth/calendar\",\n \"https://www.googleapis.com/auth/spreadsheets\",\n \"https://www.googleapis.com/auth/documents\",\n \"https://www.googleapis.com/auth/chat.messages.create\",\n];\n\n// Conservative default: enough for `gh` CLI repo/user/org operations, but\n// without `workflow` (can dispatch CI), `write:packages` (can publish\n// packages), or `project`. Operators who need those can opt in via\n// MOM_GITHUB_OAUTH_SCOPES to keep the blast radius of a compromised agent\n// host explicit and configurable.\nconst DEFAULT_GITHUB_OAUTH_SCOPES = [\"repo\", \"read:user\", \"user:email\", \"read:org\", \"gist\"];\n\nfunction resolveScopesFromEnv(envKey: string, fallback: string[]): string[] {\n const raw = process.env[envKey]?.trim();\n if (!raw) return fallback;\n\n const scopes = raw\n .split(/[\\s,]+/)\n .map((scope) => scope.trim())\n .filter(Boolean);\n\n return scopes.length > 0 ? scopes : fallback;\n}\n\nfunction resolveGoogleWorkspaceCliScopes(): string[] {\n return resolveScopesFromEnv(\n \"MOM_GOOGLE_WORKSPACE_CLI_OAUTH_SCOPES\",\n DEFAULT_GOOGLE_WORKSPACE_CLI_SCOPES,\n );\n}\n\nfunction resolveGitHubOAuthScopes(): string[] {\n return resolveScopesFromEnv(\"MOM_GITHUB_OAUTH_SCOPES\", DEFAULT_GITHUB_OAUTH_SCOPES);\n}\n\nfunction getBuiltinOAuthServices(): OAuthService[] {\n return [\n {\n id: \"github\",\n label: \"GitHub\",\n aliases: [\"github\", \"github_oauth\", \"gh_oauth\"],\n authorizationUrl: \"https://github.com/login/oauth/authorize\",\n tokenUrl: \"https://github.com/login/oauth/access_token\",\n scopes: resolveGitHubOAuthScopes(),\n clientIdEnvKey: \"GITHUB_OAUTH_CLIENT_ID\",\n clientSecretEnvKey: \"GITHUB_OAUTH_CLIENT_SECRET\",\n accessTokenEnvKey: \"GITHUB_OAUTH_ACCESS_TOKEN\",\n additionalAccessTokenEnvKeys: [\"GH_TOKEN\"],\n refreshTokenEnvKey: \"GITHUB_OAUTH_REFRESH_TOKEN\",\n },\n {\n id: \"google_workspace_cli\",\n label: \"Google Workspace CLI\",\n aliases: [\"google_workspace_cli\", \"gws\", \"googleworkspace\", \"google-workspace-cli\"],\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n scopes: resolveGoogleWorkspaceCliScopes(),\n clientIdEnvKey: \"GOOGLE_WORKSPACE_CLI_CLIENT_ID\",\n clientSecretEnvKey: \"GOOGLE_WORKSPACE_CLI_CLIENT_SECRET\",\n authorizationParams: {\n access_type: \"offline\",\n include_granted_scopes: \"true\",\n prompt: \"consent\",\n },\n fileOutput: {\n type: \"authorized_user\",\n relativePath: \"gws.json\",\n targetPath: \"/root/.config/gws/credentials.json\",\n },\n },\n ];\n}\n\nexport function getOAuthServices(): OAuthService[] {\n const raw = process.env.MOM_OAUTH_SERVICES_JSON?.trim();\n const builtins = getBuiltinOAuthServices();\n if (!raw) return builtins;\n\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return builtins;\n\n const custom = parsed\n .map((entry): OAuthService | null => {\n if (!entry || typeof entry !== \"object\") return null;\n const obj = entry as Record<string, unknown>;\n const id = typeof obj.id === \"string\" ? obj.id.trim() : \"\";\n const label = typeof obj.label === \"string\" ? obj.label.trim() : \"\";\n const authorizationUrl =\n typeof obj.authorizationUrl === \"string\" ? obj.authorizationUrl.trim() : \"\";\n const tokenUrl = typeof obj.tokenUrl === \"string\" ? obj.tokenUrl.trim() : \"\";\n const clientIdEnvKey =\n typeof obj.clientIdEnvKey === \"string\" ? obj.clientIdEnvKey.trim() : \"\";\n const clientSecretEnvKey =\n typeof obj.clientSecretEnvKey === \"string\" ? obj.clientSecretEnvKey.trim() : \"\";\n const accessTokenEnvKey =\n typeof obj.accessTokenEnvKey === \"string\" ? obj.accessTokenEnvKey.trim() : undefined;\n if (\n !id ||\n !label ||\n !authorizationUrl ||\n !tokenUrl ||\n !clientIdEnvKey ||\n !clientSecretEnvKey\n ) {\n return null;\n }\n\n let fileOutput: OAuthService[\"fileOutput\"];\n if (obj.fileOutput && typeof obj.fileOutput === \"object\") {\n const fileOutputObj = obj.fileOutput as Record<string, unknown>;\n const type = typeof fileOutputObj.type === \"string\" ? fileOutputObj.type.trim() : \"\";\n const relativePath =\n typeof fileOutputObj.relativePath === \"string\" ? fileOutputObj.relativePath.trim() : \"\";\n const targetPath =\n typeof fileOutputObj.targetPath === \"string\"\n ? fileOutputObj.targetPath.trim()\n : undefined;\n const envKey =\n typeof fileOutputObj.envKey === \"string\" ? fileOutputObj.envKey.trim() : undefined;\n if (type === \"authorized_user\" && relativePath) {\n fileOutput = { type: \"authorized_user\", relativePath, targetPath, envKey };\n }\n }\n\n return {\n id: id.toLowerCase(),\n label,\n aliases: Array.isArray(obj.aliases)\n ? obj.aliases\n .filter((v): v is string => typeof v === \"string\")\n .map((v) => v.toLowerCase())\n : [id.toLowerCase()],\n authorizationUrl,\n tokenUrl,\n scopes: Array.isArray(obj.scopes)\n ? obj.scopes.filter((v): v is string => typeof v === \"string\")\n : [],\n clientIdEnvKey,\n clientSecretEnvKey,\n accessTokenEnvKey,\n additionalAccessTokenEnvKeys: Array.isArray(obj.additionalAccessTokenEnvKeys)\n ? obj.additionalAccessTokenEnvKeys.filter((v): v is string => typeof v === \"string\")\n : undefined,\n refreshTokenEnvKey:\n typeof obj.refreshTokenEnvKey === \"string\" ? obj.refreshTokenEnvKey.trim() : undefined,\n authorizationParams:\n obj.authorizationParams && typeof obj.authorizationParams === \"object\"\n ? Object.fromEntries(\n Object.entries(obj.authorizationParams as Record<string, unknown>).filter(\n (entry): entry is [string, string] => typeof entry[1] === \"string\",\n ),\n )\n : undefined,\n fileOutput,\n };\n })\n .filter((service): service is OAuthService => service !== null);\n\n const byId = new Map<string, OAuthService>();\n for (const service of builtins) byId.set(service.id, service);\n for (const service of custom) byId.set(service.id, service);\n return [...byId.values()];\n } catch {\n return builtins;\n }\n}\n\nexport function resolveOAuthService(input: string): OAuthService | undefined {\n const normalized = input.trim().toLowerCase();\n if (!normalized) return undefined;\n return getOAuthServices().find(\n (service) => service.id === normalized || service.aliases.includes(normalized),\n );\n}\n\nexport function parseLoginCommand(text: string): ParsedLoginCommand | null {\n const tokens = text.trim().split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return null;\n\n const command = tokens[0].toLowerCase();\n if (command !== \"login\" && command !== \"/login\" && command !== \"/pi-login\") {\n return null;\n }\n\n return { command: command as \"login\" | \"/login\" | \"/pi-login\" };\n}\n"]}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,iBAAiB,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport \"./instrument.js\";\n\nimport { join, resolve } from \"path\";\nimport { readFileSync } from \"fs\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join as pathJoin } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getSessionDir,\n getThreadSessionFile,\n} from \"./session-store.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 { addLifecycleBreadcrumb, applyRunScope } from \"./sentry.js\";\nimport { ChannelStore } from \"./store.js\";\nimport * as Sentry from \"@sentry/node\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\n// Get version from package.json\nfunction getVersion(): string {\n // Try to find package.json in the dist directory or parent\n const possiblePaths = [\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"package.json\"),\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\"),\n pathJoin(process.cwd(), \"package.json\"),\n ];\n\n for (const pkgPath of possiblePaths) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.version) return pkg.version;\n } catch {\n // Continue to next path\n }\n }\n return \"unknown\";\n}\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\n\ninterface ParsedArgs {\n workingDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n showVersion?: boolean;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let downloadChannelId: string | undefined;\n let showVersion = false;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\") {\n showVersion = true;\n } else if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n showVersion,\n };\n}\n\nconst parsedArgs = parseArgs();\n\n// Handle --version\nif (parsedArgs.showVersion) {\n console.log(getVersion());\n process.exit(0);\n}\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\n \"Usage: mama [--sandbox=host|docker:<name>|firecracker:<vm-id>:<host-path>] <working-directory>\",\n );\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\nawait validateSandbox(sandbox);\n\n// ============================================================================\n// State (per channel)\n// ============================================================================\n\ninterface ChannelState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: 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 const key = sessionKey ?? channelId;\n let state = channelStates.get(key);\n if (!state) {\n const channelDir = join(workingDir, channelId);\n state = {\n running: false,\n runner: await createRunner(sandbox, key, channelId, channelDir, workingDir),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n channelStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from channelStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of channelStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n channelStates.delete(key);\n }\n }\n\n if (channelStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of channelStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = channelStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n channelStates.delete(idleSessions[i].key);\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = channelStates.get(sessionKey);\n return !!state?.running;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of channelStates) {\n if (state.running && state.startedAt) {\n // Get current step from runner\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, channelId: string, bot: Bot): Promise<void> {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(channelId, \"_Stopping..._\");\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(channelId, \"_Nothing running_\");\n }\n },\n\n forceStop(sessionKey: string): void {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n },\n\n async handleNew(sessionKey: string, channelId: string, bot: Bot): Promise<void> {\n const state = channelStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n // Channel sessions rotate via current pointer. Thread sessions reset in place.\n const channelDir = join(workingDir, channelId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(getThreadSessionFile(channelDir, sessionKey), channelDir);\n } else {\n createManagedSessionFile(getSessionDir(channelDir, sessionKey), channelDir);\n }\n\n // Remove from in-memory cache\n channelStates.delete(sessionKey);\n\n log.logInfo(`[${channelId}] Session reset: ${sessionKey}`);\n await bot.postMessage(channelId, \"Conversation reset. Send a new message to start fresh.\");\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${event.channel}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${event.channel}:${event.thread_ts ?? event.ts}`;\n const state = await getState(event.channel, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${event.channel}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: event.channel },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size + 1);\n\n const runPromise = Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { channelId: event.channel, sessionKey } },\n async () => {\n return Sentry.withScope(async (scope) => {\n const { message, responseCtx, platform } = adapters;\n applyRunScope(scope, {\n channelId: event.channel,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent: _isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: event.channel,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n const durationMs = Date.now() - state.startedAt!;\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: {\n channel: event.channel,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, {\n attributes: {\n channel: event.channel,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: event.channel,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(event.channel, state.stopMessageTs, \"_Stopped_\");\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(event.channel, \"_Stopped_\");\n }\n }\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n channelId: event.channel,\n sessionKey,\n platform: adapters.platform.name,\n messageId: adapters.message.id,\n threadTs: adapters.message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: event.channel, platform: adapters.platform.name },\n });\n log.logWarning(\n `[${event.channel}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size - 1);\n evictIdleSessions();\n }\n });\n },\n );\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nconst sandboxDesc =\n sandbox.type === \"host\"\n ? \"host\"\n : sandbox.type === \"docker\"\n ? `docker:${sandbox.container}`\n : `firecracker:${sandbox.vmId}`;\nlog.logStartup(workingDir, sandboxDesc);\n\n// Create platform bots\nconst bots: Bot[] = [];\nconst botsByPlatform: Record<string, Bot> = {};\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n const slackBot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n bots.push(slackBot);\n botsByPlatform.slack = slackBot;\n log.logInfo(\"Platform: Slack\");\n}\nif (hasTelegram) {\n const telegramBot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n bots.push(telegramBot);\n botsByPlatform.telegram = telegramBot;\n log.logInfo(\"Platform: Telegram\");\n}\nif (hasDiscord) {\n const discordBot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n bots.push(discordBot);\n botsByPlatform.discord = discordBot;\n log.logInfo(\"Platform: Discord\");\n}\n\n// Start events watcher with explicit platform routing\nconst eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);\nconst slackBot = botsByPlatform.slack as SlackBotClass | undefined;\nif (slackBot) {\n slackBot.setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nasync function shutdown(): Promise<void> {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n await Sentry.close(5000);\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\n// Start all bots\nawait Promise.all(\n bots.map((bot) =>\n bot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n }),\n ),\n);\n"]}
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,iBAAiB,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport \"./instrument.js\";\n\nimport { join, resolve } from \"path\";\nimport { mkdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join as pathJoin } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n} from \"./session-store.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { FileUserBindingStore } from \"./bindings.js\";\nimport { startLinkServer } from \"./link-server.js\";\nimport { parseLoginCommand } from \"./login.js\";\nimport { InMemoryLinkTokenStore } from \"./link-token.js\";\nimport { DockerContainerManager } from \"./provisioner.js\";\nimport { SandboxError, parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { FileVaultManager } from \"./vault.js\";\nimport {\n createManagedVaultEntry,\n ensureSandboxVaultEntry,\n resolveActorVaultKey,\n} from \"./vault-routing.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"./sentry.js\";\nimport { ChannelStore } from \"./store.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"./ui-copy.js\";\nimport * as Sentry from \"@sentry/node\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\n// Get version from package.json\nfunction getVersion(): string {\n // Try to find package.json in the dist directory or parent\n const possiblePaths = [\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"package.json\"),\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\"),\n pathJoin(process.cwd(), \"package.json\"),\n ];\n\n for (const pkgPath of possiblePaths) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.version) return pkg.version;\n } catch {\n // Continue to next path\n }\n }\n return \"unknown\";\n}\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\nconst MOM_LINK_URL = process.env.MOM_LINK_URL;\nconst MOM_LINK_PORT = process.env.MOM_LINK_PORT\n ? parseInt(process.env.MOM_LINK_PORT, 10)\n : MOM_LINK_URL\n ? 8181\n : undefined;\n\ninterface ParsedArgs {\n workingDir?: string;\n stateDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n showVersion?: boolean;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let stateDirArg: string | undefined;\n let downloadChannelId: string | undefined;\n let showVersion = false;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\") {\n showVersion = true;\n } else if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--state-dir=\")) {\n stateDirArg = arg.slice(\"--state-dir=\".length);\n } else if (arg === \"--state-dir\") {\n stateDirArg = args[++i];\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n stateDir: stateDirArg ? resolve(stateDirArg) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n showVersion,\n };\n}\n\nconst WORLD_WRITABLE_MODE = 0o002;\n\nfunction ensureSecureStateDir(path: string): void {\n let stat;\n try {\n stat = statSync(path);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n mkdirSync(path, { recursive: true, mode: 0o700 });\n return;\n }\n console.error(`Error: cannot access --state-dir ${path}: ${(err as Error).message}`);\n process.exit(1);\n }\n\n if (!stat.isDirectory()) {\n console.error(`Error: --state-dir ${path} exists but is not a directory`);\n process.exit(1);\n }\n\n if (stat.mode & WORLD_WRITABLE_MODE) {\n console.error(\n `Error: --state-dir ${path} is world-writable (mode ${(stat.mode & 0o777).toString(8)}). ` +\n `Credentials stored there would be exposed to other local users. ` +\n `Fix with: chmod 0700 ${path}`,\n );\n process.exit(1);\n }\n\n const euid = typeof process.geteuid === \"function\" ? process.geteuid() : undefined;\n if (euid !== undefined && stat.uid !== euid) {\n console.error(\n `Error: --state-dir ${path} is owned by uid ${stat.uid} but mama is running as uid ${euid}. ` +\n `Run mama as the directory owner or point --state-dir at a directory you own.`,\n );\n process.exit(1);\n }\n}\n\nfunction handleStartupError(error: unknown): never {\n if (error instanceof SandboxError) {\n for (const line of error.formatForCli()) {\n console.error(line);\n }\n process.exit(1);\n }\n throw error;\n}\n\nlet parsedArgs: ParsedArgs;\ntry {\n parsedArgs = parseArgs();\n} catch (error) {\n handleStartupError(error);\n}\n\n// Handle --version\nif (parsedArgs.showVersion) {\n console.log(getVersion());\n process.exit(0);\n}\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\n \"Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>\",\n );\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\nconst stateDir = parsedArgs.stateDir ?? join(homedir(), \".mama\");\nprocess.env.MAMA_STATE_DIR = stateDir;\nensureSecureStateDir(stateDir);\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\ntry {\n await validateSandbox(sandbox);\n} catch (error) {\n handleStartupError(error);\n}\n\nconst vaultManager = new FileVaultManager(stateDir);\nif (vaultManager.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Vault system enabled. Container vault active.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Vault system enabled. Per-user credential routing active.\"\n : \" Vault system enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst bindingStore = new FileUserBindingStore(stateDir);\nif (bindingStore.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Binding store enabled. Container mode uses the container vault.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Binding store enabled. Platform user → vault routing active.\"\n : \" Binding store enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst provisioner =\n sandbox.type === \"image\" ? new DockerContainerManager(sandbox.image, workingDir) : undefined;\n\nconst linkTokenStore = new InMemoryLinkTokenStore();\nsetInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();\n\nfunction normalizeLoginBaseUrl(): string | undefined {\n if (MOM_LINK_URL) {\n return MOM_LINK_URL.replace(/\\/+$/, \"\");\n }\n if (MOM_LINK_PORT) {\n return `http://localhost:${MOM_LINK_PORT}`;\n }\n return undefined;\n}\n\nfunction isPrivateConversation(event: BotEvent): boolean {\n return (\n event.conversationKind === \"direct\" ||\n event.type === \"dm\" ||\n event.sessionKey === event.conversationId\n );\n}\n\nfunction ensureLoginVault(platform: string, platformUserId: string): string {\n const vaultId = resolveActorVaultKey(\n sandbox,\n vaultManager,\n bindingStore,\n platform,\n platformUserId,\n );\n\n ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);\n if (sandbox.type !== \"container\" && sandbox.type !== \"image\") {\n vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));\n }\n\n return vaultId;\n}\n\nasync function replyWithContext(\n responseCtx: BotAdapters[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nasync function handleLoginCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n const parsed = parseLoginCommand(commandText);\n if (!parsed) return false;\n\n if (!privateConversation) {\n await replyWithContext(\n responseCtx,\n \"為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。\",\n );\n return true;\n }\n\n const baseUrl = normalizeLoginBaseUrl();\n if (!baseUrl) {\n await replyWithContext(\n responseCtx,\n \"Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n let vaultId: string;\n try {\n vaultId = ensureLoginVault(platform, platformUserId);\n } catch (error) {\n log.logWarning(\n `[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`,\n error instanceof Error ? error.message : String(error),\n );\n await replyWithContext(\n responseCtx,\n \"Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。\",\n );\n return true;\n }\n\n const token = linkTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n vaultId,\n \"\",\n );\n const vaultLabel = sandbox.type === \"container\" ? `container vault (${vaultId})` : \"your vault\";\n await replyWithContext(\n responseCtx,\n `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\\n${baseUrl}/link?token=${token.token}`,\n );\n return true;\n}\n\n// ============================================================================\n// State (per conversation)\n// ============================================================================\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nconst conversationStates = new Map<string, ConversationState>();\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/** Idle timeout for managed image containers (10 minutes) */\nconst IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;\n\nif (provisioner) {\n await provisioner.reconcile();\n await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);\n setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();\n}\n\nasync function getState(conversationId: string, sessionKey?: string): Promise<ConversationState> {\n const key = sessionKey ?? conversationId;\n let state = conversationStates.get(key);\n if (!state) {\n const conversationDir = join(workingDir, conversationId);\n state = {\n running: false,\n runner: await createRunner(\n sandbox,\n key,\n conversationId,\n conversationDir,\n workingDir,\n vaultManager,\n bindingStore,\n provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n conversationStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from conversationStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n conversationStates.delete(key);\n }\n }\n\n if (conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n conversationStates.delete(idleSessions[i].key);\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = conversationStates.get(sessionKey);\n return !!state?.running;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of conversationStates) {\n if (state.running && state.startedAt) {\n // Get current step from runner\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n },\n\n forceStop(sessionKey: string): void {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n },\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n // Conversation sessions rotate via current pointer. Thread sessions reset in place.\n const conversationDir = join(workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n // Remove from in-memory cache\n conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n const conversationId = event.conversationId;\n\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const handledLogin = await handleLoginCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n adapters.responseCtx,\n event.text,\n isPrivateConversation(event),\n );\n if (handledLogin) return;\n\n const state = await getState(conversationId, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size + 1);\n\n const runPromise = Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () => {\n return Sentry.withScope(async (scope) => {\n const { message, responseCtx, platform } = adapters;\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent: _isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n const durationMs = Date.now() - state.startedAt!;\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, {\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: adapters.platform.name,\n messageId: adapters.message.id,\n threadTs: adapters.message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: adapters.platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size - 1);\n evictIdleSessions();\n }\n });\n },\n );\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nconst sandboxDesc =\n sandbox.type === \"host\"\n ? \"host\"\n : sandbox.type === \"container\"\n ? `container:${sandbox.container}`\n : sandbox.type === \"image\"\n ? `image:${sandbox.image}`\n : `firecracker:${sandbox.vmId}`;\nlog.logStartup(workingDir, sandboxDesc);\n\n// Create platform bots\nconst bots: Bot[] = [];\nconst botsByPlatform: Record<string, Bot> = {};\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n const slackBot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n bots.push(slackBot);\n botsByPlatform.slack = slackBot;\n log.logInfo(\"Platform: Slack\");\n}\nif (hasTelegram) {\n const telegramBot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n bots.push(telegramBot);\n botsByPlatform.telegram = telegramBot;\n log.logInfo(\"Platform: Telegram\");\n}\nif (hasDiscord) {\n const discordBot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n bots.push(discordBot);\n botsByPlatform.discord = discordBot;\n log.logInfo(\"Platform: Discord\");\n}\n\nif (MOM_LINK_PORT) {\n startLinkServer(\n MOM_LINK_PORT,\n linkTokenStore,\n vaultManager,\n async (platform, conversationId, message) => {\n const bot = botsByPlatform[platform];\n if (bot) await bot.postMessage(conversationId, message);\n },\n );\n}\n\n// Start events watcher with explicit platform routing\nconst eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);\nconst slackBot = botsByPlatform.slack as SlackBotClass | undefined;\nif (slackBot) {\n slackBot.setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nasync function shutdown(): Promise<void> {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n await Sentry.close(5000);\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\n// Start all bots\nawait Promise.all(\n bots.map((bot) =>\n bot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n }),\n ),\n);\n"]}
|