moltbot-termux 2026.1.28-3-pre → 2026.1.28-4

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/CHANGELOG.md CHANGED
@@ -66,11 +66,14 @@ Status: beta.
66
66
  - Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.
67
67
  - macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.
68
68
  - CLI: use Node's module compile cache for faster startup. (#2808) Thanks @pi0.
69
+ - Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam.
69
70
 
70
71
  ### Breaking
71
72
  - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
72
73
 
73
74
  ### Fixes
75
+ - Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald.
76
+ - Agents: align MiniMax base URL test expectation with default provider config. (#3131) Thanks @bonald.
74
77
  - Agents: prevent retries on oversized image errors and surface size limits. (#2871) Thanks @Suksham-sharma.
75
78
  - Agents: inherit provider baseUrl/api for inline models. (#2740) Thanks @lploc94.
76
79
  - Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355.
@@ -78,6 +81,7 @@ Status: beta.
78
81
  - Web UI: auto-expand the chat compose textarea while typing (with sensible max height). (#2950) Thanks @shivamraut101.
79
82
  - Gateway: prevent crashes on transient network errors (fetch failures, timeouts, DNS). Added fatal error detection to only exit on truly critical errors. Fixes #2895, #2879, #2873. (#2980) Thanks @elliotsecops.
80
83
  - Agents: guard channel tool listActions to avoid plugin crashes. (#2859) Thanks @mbelinky.
84
+ - Discord: stop resolveDiscordTarget from passing directory params into messaging target parsers. Fixes #3167. Thanks @thewilloftheshadow.
81
85
  - Discord: avoid resolving bare channel names to user DMs when a username matches. Thanks @thewilloftheshadow.
82
86
  - Discord: fix directory config type import for target resolution. Thanks @thewilloftheshadow.
83
87
  - Providers: update MiniMax API endpoint and compatibility mode. (#3064) Thanks @hlbbbbbbb.
package/README.md CHANGED
@@ -46,18 +46,67 @@ Model note: while any model is supported, I strongly recommend **Anthropic Pro/M
46
46
 
47
47
  ## Install (recommended)
48
48
 
49
+ **Using install script:**
50
+ ```bash
51
+ curl -s https://explysm.github.io/moltbot-termux/install.sh | sh
52
+ ```
53
+ This script handles:
54
+ - Node 22
55
+ - npm
56
+ - pnpm
57
+ - moltbot-termux
58
+ - Clipboard fix
59
+
60
+ **Using NPM: (Not recommended, requires manual clipboard fix)**
61
+
49
62
  Runtime: **Node ≥22**.
50
63
 
51
64
  ```bash
52
65
  npm install -g moltbot-termux
53
66
  # or: pnpm add -g moltbot-termux
54
67
 
55
- moltbot onboard --install-daemon
68
+ moltbot onboard
69
+ ```
70
+ Gateway daemon installation does not work on Termux, but you can run it manually:
71
+ ```bash
72
+ moltbot gateway --port 18789 --verbose
56
73
  ```
57
-
58
- The wizard installs the Gateway daemon (launchd/systemd user service) so it stays running.
59
74
  Legacy note: `clawdbot` remains available as a compatibility shim.
60
75
 
76
+ **Clipboard fix:**
77
+ Save this as fix.sh
78
+ ```bash
79
+ #!/bin/bash
80
+
81
+ CLIPBOARD_FIX_PATH=$(find "$HOME/.local/share/pnpm/global" -name "index.js" -path "*/@mariozechner/clipboard/*" | head -n 1)
82
+
83
+ if [ -n "$CLIPBOARD_FIX_PATH" ]; then
84
+ echo "Found clipboard package at: $CLIPBOARD_FIX_PATH"
85
+ cat > "$CLIPBOARD_FIX_PATH" <<EOF
86
+ module.exports = {
87
+ availableFormats: () => [],
88
+ getText: () => "",
89
+ setText: () => {},
90
+ hasText: () => false,
91
+ getImageBinary: () => null,
92
+ getImageBase64: () => null,
93
+ setImageBinary: () => {},
94
+ setImageBase64: () => {},
95
+ hasImage: () => false,
96
+ getHtml: () => "",
97
+ setHtml: () => {},
98
+ hasHtml: () => false,
99
+ getRtf: () => "",
100
+ setRtf: () => {},
101
+ hasRtf: () => false,
102
+ clear: () => {},
103
+ watch: () => {},
104
+ callThreadsafeFunction: () => {}
105
+ };
106
+ EOF
107
+ echo "Clipboard fix applied successfully."
108
+ ```
109
+
61
110
  ## Quick start (TL;DR)
62
111
 
63
112
  Runtime: **Node ≥22**.
@@ -65,7 +114,7 @@ Runtime: **Node ≥22**.
65
114
  Full beginner guide (auth, pairing, channels): [Getting started](https://docs.molt.bot/start/getting-started)
66
115
 
67
116
  ```bash
68
- moltbot onboard --install-daemon
117
+ moltbot onboard
69
118
 
70
119
  moltbot gateway --port 18789 --verbose
71
120
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.1.28-3-pre",
3
- "commit": "ea53cbc3cebd63f309cc698a7de6d7c1c83d8811",
4
- "builtAt": "2026-01-28T06:43:23.890Z"
2
+ "version": "2026.1.28-4",
3
+ "commit": "4f4d75e0629a9c68ecc0126e7062b200c071bfdd",
4
+ "builtAt": "2026-01-29T02:53:50.946Z"
5
5
  }
@@ -1 +1 @@
1
- 6a6fa582e97f163cdac952fc1a2babb57bb4c2c08e0e59160d86e03ea11b5a5c
1
+ 1ebedd0b9b5770fc0a27d08fa3c730f4c5746893745477e92468598a63777554
@@ -52,6 +52,7 @@ const LOBSTER_ASCII = [
52
52
  "██░███░██░▀▀▀░██░▀▀░███░████░▀▀░██░▀▀▀░███░████",
53
53
  "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
54
54
  " 🦞 FRESH DAILY 🦞 ",
55
+ " ",
55
56
  ];
56
57
  export function formatCliBannerArt(options = {}) {
57
58
  const rich = options.richTty ?? isRich();
@@ -156,6 +156,11 @@ const entries = [
156
156
  name: "pairing",
157
157
  description: "Pairing helpers",
158
158
  register: async (program) => {
159
+ // Initialize plugins before registering pairing CLI.
160
+ // The pairing CLI calls listPairingChannels() at registration time,
161
+ // which requires the plugin registry to be populated with channel plugins.
162
+ const { registerPluginCliCommands } = await import("../../plugins/cli.js");
163
+ registerPluginCliCommands(program, await loadConfig());
159
164
  const mod = await import("../pairing-cli.js");
160
165
  mod.registerPairingCli(program);
161
166
  },
@@ -87,7 +87,7 @@ export async function noteSecurityWarnings(cfg) {
87
87
  warnings.push(` ${params.approveHint}`);
88
88
  }
89
89
  if (dmScope === "main" && isMultiUserDm) {
90
- warnings.push(`- ${params.label} DMs: multiple senders share the main session; set session.dmScope="per-channel-peer" to isolate sessions.`);
90
+ warnings.push(`- ${params.label} DMs: multiple senders share the main session; set session.dmScope="per-channel-peer" (or "per-account-channel-peer" for multi-account channels) to isolate sessions.`);
91
91
  }
92
92
  };
93
93
  for (const plugin of listChannelPlugins()) {
@@ -125,7 +125,7 @@ async function noteChannelPrimer(prompter, channels) {
125
125
  "DM security: default is pairing; unknown DMs get a pairing code.",
126
126
  `Approve with: ${formatCliCommand("moltbot pairing approve <channel> <code>")}`,
127
127
  'Public DMs require dmPolicy="open" + allowFrom=["*"].',
128
- 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.',
128
+ 'Multi-user DMs: set session.dmScope="per-channel-peer" (or "per-account-channel-peer" for multi-account channels) to isolate sessions.',
129
129
  `Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
130
130
  "",
131
131
  ...channelLines,
@@ -162,7 +162,7 @@ async function maybeConfigureDmPolicies(params) {
162
162
  `Approve: ${formatCliCommand(`moltbot pairing approve ${policy.channel} <code>`)}`,
163
163
  `Allowlist DMs: ${policy.policyKey}="allowlist" + ${policy.allowFromKey} entries.`,
164
164
  `Public DMs: ${policy.policyKey}="open" + ${policy.allowFromKey} includes "*".`,
165
- 'Multi-user DMs: set session.dmScope="per-channel-peer" to isolate sessions.',
165
+ 'Multi-user DMs: set session.dmScope="per-channel-peer" (or "per-account-channel-peer" for multi-account channels) to isolate sessions.',
166
166
  `Docs: ${formatDocsLink("/start/pairing", "start/pairing")}`,
167
167
  ].join("\n"), `${policy.label} DM access`);
168
168
  return (await prompter.select({
@@ -58,7 +58,8 @@ export function printWizardHeader(runtime) {
58
58
  "██░█░█░██░███░██░██████░████░▄▄▀██░███░███░████",
59
59
  "██░███░██░▀▀▀░██░▀▀░███░████░▀▀░██░▀▀▀░███░████",
60
60
  "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
61
- " 🦞 FRESH DAILY 🦞 ",
61
+ " 🦞 FRESH DAILY 🦞 ",
62
+ " ",
62
63
  ].join("\n");
63
64
  runtime.log(header);
64
65
  }
@@ -460,7 +460,7 @@ const FIELD_HELP = {
460
460
  "commands.debug": "Allow /debug chat command for runtime-only overrides (default: false).",
461
461
  "commands.restart": "Allow /restart and gateway restart tool actions (default: false).",
462
462
  "commands.useAccessGroups": "Enforce access-group allowlists/policies for commands.",
463
- "session.dmScope": 'DM session scoping: "main" keeps continuity; "per-peer" or "per-channel-peer" isolates DM history (recommended for shared inboxes).',
463
+ "session.dmScope": 'DM session scoping: "main" keeps continuity; "per-peer", "per-channel-peer", or "per-account-channel-peer" isolates DM history (recommended for shared inboxes/multi-account).',
464
464
  "session.identityLinks": "Map canonical identities to provider-prefixed peer IDs for DM session linking (example: telegram:123456).",
465
465
  "channels.telegram.configWrites": "Allow Telegram to write config in response to channel events/commands (default: true).",
466
466
  "channels.slack.configWrites": "Allow Slack to write config in response to channel events/commands (default: true).",
@@ -11,7 +11,12 @@ export const SessionSchema = z
11
11
  .object({
12
12
  scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(),
13
13
  dmScope: z
14
- .union([z.literal("main"), z.literal("per-peer"), z.literal("per-channel-peer")])
14
+ .union([
15
+ z.literal("main"),
16
+ z.literal("per-peer"),
17
+ z.literal("per-channel-peer"),
18
+ z.literal("per-account-channel-peer"),
19
+ ])
15
20
  .optional(),
16
21
  identityLinks: z.record(z.string(), z.array(z.string())).optional(),
17
22
  resetTriggers: z.array(z.string()).optional(),
@@ -81,17 +81,19 @@ export async function parseAndResolveRecipient(raw, accountId) {
81
81
  const cfg = loadConfig();
82
82
  const accountInfo = resolveDiscordAccount({ cfg, accountId });
83
83
  // First try to resolve using directory lookup (handles usernames)
84
+ const trimmed = raw.trim();
85
+ const parseOptions = {
86
+ ambiguousMessage: `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`,
87
+ };
84
88
  const resolved = await resolveDiscordTarget(raw, {
85
89
  cfg,
86
90
  accountId: accountInfo.accountId,
87
- });
91
+ }, parseOptions);
88
92
  if (resolved) {
89
93
  return { kind: resolved.kind, id: resolved.id };
90
94
  }
91
95
  // Fallback to standard parsing (for channels, etc.)
92
- const parsed = parseDiscordTarget(raw, {
93
- ambiguousMessage: `Ambiguous Discord recipient "${raw.trim()}". Use "user:${raw.trim()}" for DMs or "channel:${raw.trim()}" for channel messages.`,
94
- });
96
+ const parsed = parseDiscordTarget(raw, parseOptions);
95
97
  if (!parsed) {
96
98
  throw new Error("Recipient is required for Discord sends");
97
99
  }
@@ -48,16 +48,17 @@ export function resolveDiscordChannelId(raw) {
48
48
  *
49
49
  * @param raw - The username or raw target string (e.g., "john.doe")
50
50
  * @param options - Directory configuration params (cfg, accountId, limit)
51
- * @param parseOptions - Optional parsing options (defaultKind, ambiguousMessage)
51
+ * @param parseOptions - Messaging target parsing options (defaults, ambiguity message)
52
52
  * @returns Parsed MessagingTarget with user ID, or undefined if not found
53
53
  */
54
54
  export async function resolveDiscordTarget(raw, options, parseOptions = {}) {
55
55
  const trimmed = raw.trim();
56
56
  if (!trimmed)
57
57
  return undefined;
58
- const shouldLookup = isExplicitUserLookup(trimmed, parseOptions);
58
+ const likelyUsername = isLikelyUsername(trimmed);
59
+ const shouldLookup = isExplicitUserLookup(trimmed, parseOptions) || likelyUsername;
59
60
  const directParse = safeParseDiscordTarget(trimmed, parseOptions);
60
- if (directParse && directParse.kind !== "channel") {
61
+ if (directParse && directParse.kind !== "channel" && !likelyUsername) {
61
62
  return directParse;
62
63
  }
63
64
  if (!shouldLookup) {
@@ -77,7 +78,7 @@ export async function resolveDiscordTarget(raw, options, parseOptions = {}) {
77
78
  return buildMessagingTarget("user", userId, trimmed);
78
79
  }
79
80
  }
80
- catch (error) {
81
+ catch {
81
82
  // Directory lookup failed - fall through to parse as-is
82
83
  // This preserves existing behavior for channel names
83
84
  }
@@ -107,3 +108,15 @@ function isExplicitUserLookup(input, options) {
107
108
  }
108
109
  return false;
109
110
  }
111
+ /**
112
+ * Check if a string looks like a Discord username (not a mention, prefix, or ID).
113
+ * Usernames typically don't start with special characters except underscore.
114
+ */
115
+ function isLikelyUsername(input) {
116
+ // Skip if it's already a known format
117
+ if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) {
118
+ return false;
119
+ }
120
+ // Likely a username if it doesn't match known patterns
121
+ return true;
122
+ }
@@ -23,7 +23,7 @@ Automatically saves session context to your workspace memory when you issue the
23
23
  When you run `/new` to start a fresh session:
24
24
 
25
25
  1. **Finds the previous session** - Uses the pre-reset session entry to locate the correct transcript
26
- 2. **Extracts conversation** - Reads the last 15 lines of conversation from the session
26
+ 2. **Extracts conversation** - Reads the last N user/assistant messages from the session (default: 15, configurable)
27
27
  3. **Generates descriptive slug** - Uses LLM to create a meaningful filename slug based on conversation content
28
28
  4. **Saves to memory** - Creates a new file at `<workspace>/memory/YYYY-MM-DD-slug.md`
29
29
  5. **Sends confirmation** - Notifies you with the file path
@@ -57,7 +57,30 @@ The hook uses your configured LLM provider to generate slugs, so it works with a
57
57
 
58
58
  ## Configuration
59
59
 
60
- No additional configuration required. The hook automatically:
60
+ The hook supports optional configuration:
61
+
62
+ | Option | Type | Default | Description |
63
+ | ---------- | ------ | ------- | --------------------------------------------------------------- |
64
+ | `messages` | number | 15 | Number of user/assistant messages to include in the memory file |
65
+
66
+ Example configuration:
67
+
68
+ ```json
69
+ {
70
+ "hooks": {
71
+ "internal": {
72
+ "entries": {
73
+ "session-memory": {
74
+ "enabled": true,
75
+ "messages": 25
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ The hook automatically:
61
84
 
62
85
  - Uses your workspace directory (`~/clawd` by default)
63
86
  - Uses your configured LLM for slug generation
@@ -7,20 +7,20 @@
7
7
  import fs from "node:fs/promises";
8
8
  import path from "node:path";
9
9
  import os from "node:os";
10
+ import { fileURLToPath } from "node:url";
10
11
  import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js";
11
12
  import { resolveAgentIdFromSessionKey } from "../../../routing/session-key.js";
13
+ import { resolveHookConfig } from "../../config.js";
12
14
  /**
13
15
  * Read recent messages from session file for slug generation
14
16
  */
15
- async function getRecentSessionContent(sessionFilePath) {
17
+ async function getRecentSessionContent(sessionFilePath, messageCount = 15) {
16
18
  try {
17
19
  const content = await fs.readFile(sessionFilePath, "utf-8");
18
20
  const lines = content.trim().split("\n");
19
- // Get last 15 lines (recent conversation)
20
- const recentLines = lines.slice(-15);
21
- // Parse JSONL and extract messages
22
- const messages = [];
23
- for (const line of recentLines) {
21
+ // Parse JSONL and extract user/assistant messages first
22
+ const allMessages = [];
23
+ for (const line of lines) {
24
24
  try {
25
25
  const entry = JSON.parse(line);
26
26
  // Session files have entries with type="message" containing a nested message object
@@ -33,7 +33,7 @@ async function getRecentSessionContent(sessionFilePath) {
33
33
  ? msg.content.find((c) => c.type === "text")?.text
34
34
  : msg.content;
35
35
  if (text && !text.startsWith("/")) {
36
- messages.push(`${role}: ${text}`);
36
+ allMessages.push(`${role}: ${text}`);
37
37
  }
38
38
  }
39
39
  }
@@ -42,7 +42,9 @@ async function getRecentSessionContent(sessionFilePath) {
42
42
  // Skip invalid JSON lines
43
43
  }
44
44
  }
45
- return messages.join("\n");
45
+ // Then slice to get exactly messageCount messages
46
+ const recentMessages = allMessages.slice(-messageCount);
47
+ return recentMessages.join("\n");
46
48
  }
47
49
  catch {
48
50
  return null;
@@ -77,18 +79,23 @@ const saveSessionToMemory = async (event) => {
77
79
  console.log("[session-memory] Current sessionFile:", currentSessionFile);
78
80
  console.log("[session-memory] cfg present:", !!cfg);
79
81
  const sessionFile = currentSessionFile || undefined;
82
+ // Read message count from hook config (default: 15)
83
+ const hookConfig = resolveHookConfig(cfg, "session-memory");
84
+ const messageCount = typeof hookConfig?.messages === "number" && hookConfig.messages > 0
85
+ ? hookConfig.messages
86
+ : 15;
80
87
  let slug = null;
81
88
  let sessionContent = null;
82
89
  if (sessionFile) {
83
90
  // Get recent conversation content
84
- sessionContent = await getRecentSessionContent(sessionFile);
91
+ sessionContent = await getRecentSessionContent(sessionFile, messageCount);
85
92
  console.log("[session-memory] sessionContent length:", sessionContent?.length || 0);
86
93
  if (sessionContent && cfg) {
87
94
  console.log("[session-memory] Calling generateSlugViaLLM...");
88
95
  // Dynamically import the LLM slug generator (avoids module caching issues)
89
96
  // When compiled, handler is at dist/hooks/bundled/session-memory/handler.js
90
97
  // Going up ../.. puts us at dist/hooks/, so just add llm-slug-generator.js
91
- const moltbotRoot = path.resolve(path.dirname(import.meta.url.replace("file://", "")), "../..");
98
+ const moltbotRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
92
99
  const slugGenPath = path.join(moltbotRoot, "llm-slug-generator.js");
93
100
  const { generateSlugViaLLM } = await import(slugGenPath);
94
101
  // Use LLM to generate a descriptive slug
@@ -68,6 +68,7 @@ function buildBaseSessionKey(params) {
68
68
  return buildAgentSessionKey({
69
69
  agentId: params.agentId,
70
70
  channel: params.channel,
71
+ accountId: params.accountId,
71
72
  peer: params.peer,
72
73
  dmScope: params.cfg.session?.dmScope ?? "main",
73
74
  identityLinks: params.cfg.session?.identityLinks,
@@ -147,6 +148,7 @@ async function resolveSlackSession(params) {
147
148
  cfg: params.cfg,
148
149
  agentId: params.agentId,
149
150
  channel: "slack",
151
+ accountId: params.accountId,
150
152
  peer,
151
153
  });
152
154
  const threadId = normalizeThreadId(params.threadId ?? params.replyToId);
@@ -181,6 +183,7 @@ function resolveDiscordSession(params) {
181
183
  cfg: params.cfg,
182
184
  agentId: params.agentId,
183
185
  channel: "discord",
186
+ accountId: params.accountId,
184
187
  peer,
185
188
  });
186
189
  const explicitThreadId = normalizeThreadId(params.threadId);
@@ -225,6 +228,7 @@ function resolveTelegramSession(params) {
225
228
  cfg: params.cfg,
226
229
  agentId: params.agentId,
227
230
  channel: "telegram",
231
+ accountId: params.accountId,
228
232
  peer,
229
233
  });
230
234
  return {
@@ -250,6 +254,7 @@ function resolveWhatsAppSession(params) {
250
254
  cfg: params.cfg,
251
255
  agentId: params.agentId,
252
256
  channel: "whatsapp",
257
+ accountId: params.accountId,
253
258
  peer,
254
259
  });
255
260
  return {
@@ -273,6 +278,7 @@ function resolveSignalSession(params) {
273
278
  cfg: params.cfg,
274
279
  agentId: params.agentId,
275
280
  channel: "signal",
281
+ accountId: params.accountId,
276
282
  peer,
277
283
  });
278
284
  return {
@@ -307,6 +313,7 @@ function resolveSignalSession(params) {
307
313
  cfg: params.cfg,
308
314
  agentId: params.agentId,
309
315
  channel: "signal",
316
+ accountId: params.accountId,
310
317
  peer,
311
318
  });
312
319
  return {
@@ -329,6 +336,7 @@ function resolveIMessageSession(params) {
329
336
  cfg: params.cfg,
330
337
  agentId: params.agentId,
331
338
  channel: "imessage",
339
+ accountId: params.accountId,
332
340
  peer,
333
341
  });
334
342
  return {
@@ -352,6 +360,7 @@ function resolveIMessageSession(params) {
352
360
  cfg: params.cfg,
353
361
  agentId: params.agentId,
354
362
  channel: "imessage",
363
+ accountId: params.accountId,
355
364
  peer,
356
365
  });
357
366
  const toPrefix = parsed.kind === "chat_id"
@@ -379,6 +388,7 @@ function resolveMatrixSession(params) {
379
388
  cfg: params.cfg,
380
389
  agentId: params.agentId,
381
390
  channel: "matrix",
391
+ accountId: params.accountId,
382
392
  peer,
383
393
  });
384
394
  return {
@@ -410,6 +420,7 @@ function resolveMSTeamsSession(params) {
410
420
  cfg: params.cfg,
411
421
  agentId: params.agentId,
412
422
  channel: "msteams",
423
+ accountId: params.accountId,
413
424
  peer,
414
425
  });
415
426
  return {
@@ -443,6 +454,7 @@ function resolveMattermostSession(params) {
443
454
  cfg: params.cfg,
444
455
  agentId: params.agentId,
445
456
  channel: "mattermost",
457
+ accountId: params.accountId,
446
458
  peer,
447
459
  });
448
460
  const threadId = normalizeThreadId(params.replyToId ?? params.threadId);
@@ -484,6 +496,7 @@ function resolveBlueBubblesSession(params) {
484
496
  cfg: params.cfg,
485
497
  agentId: params.agentId,
486
498
  channel: "bluebubbles",
499
+ accountId: params.accountId,
487
500
  peer,
488
501
  });
489
502
  return {
@@ -508,6 +521,7 @@ function resolveNextcloudTalkSession(params) {
508
521
  cfg: params.cfg,
509
522
  agentId: params.agentId,
510
523
  channel: "nextcloud-talk",
524
+ accountId: params.accountId,
511
525
  peer,
512
526
  });
513
527
  return {
@@ -532,6 +546,7 @@ function resolveZaloSession(params) {
532
546
  cfg: params.cfg,
533
547
  agentId: params.agentId,
534
548
  channel: "zalo",
549
+ accountId: params.accountId,
535
550
  peer,
536
551
  });
537
552
  return {
@@ -557,6 +572,7 @@ function resolveZalouserSession(params) {
557
572
  cfg: params.cfg,
558
573
  agentId: params.agentId,
559
574
  channel: "zalouser",
575
+ accountId: params.accountId,
560
576
  peer,
561
577
  });
562
578
  return {
@@ -577,6 +593,7 @@ function resolveNostrSession(params) {
577
593
  cfg: params.cfg,
578
594
  agentId: params.agentId,
579
595
  channel: "nostr",
596
+ accountId: params.accountId,
580
597
  peer,
581
598
  });
582
599
  return {
@@ -635,6 +652,7 @@ function resolveTlonSession(params) {
635
652
  cfg: params.cfg,
636
653
  agentId: params.agentId,
637
654
  channel: "tlon",
655
+ accountId: params.accountId,
638
656
  peer,
639
657
  });
640
658
  return {
@@ -11,7 +11,10 @@ const EXT_BY_MIME = {
11
11
  "image/gif": ".gif",
12
12
  "audio/ogg": ".ogg",
13
13
  "audio/mpeg": ".mp3",
14
+ "audio/x-m4a": ".m4a",
15
+ "audio/mp4": ".m4a",
14
16
  "video/mp4": ".mp4",
17
+ "video/quicktime": ".mov",
15
18
  "application/pdf": ".pdf",
16
19
  "application/json": ".json",
17
20
  "application/zip": ".zip",
@@ -27,6 +27,7 @@ export function buildAgentSessionKey(params) {
27
27
  agentId: params.agentId,
28
28
  mainKey: DEFAULT_MAIN_KEY,
29
29
  channel,
30
+ accountId: params.accountId,
30
31
  peerKind: peer?.kind ?? "dm",
31
32
  peerId: peer ? normalizeId(peer.id) || "unknown" : null,
32
33
  dmScope: params.dmScope,
@@ -98,6 +99,7 @@ export function resolveAgentRoute(input) {
98
99
  const sessionKey = buildAgentSessionKey({
99
100
  agentId: resolvedAgentId,
100
101
  channel,
102
+ accountId,
101
103
  peer,
102
104
  dmScope,
103
105
  identityLinks,
@@ -99,6 +99,11 @@ export function buildAgentPeerSessionKey(params) {
99
99
  if (linkedPeerId)
100
100
  peerId = linkedPeerId;
101
101
  peerId = peerId.toLowerCase();
102
+ if (dmScope === "per-account-channel-peer" && peerId) {
103
+ const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
104
+ const accountId = normalizeAccountId(params.accountId);
105
+ return `agent:${normalizeAgentId(params.agentId)}:${channel}:${accountId}:dm:${peerId}`;
106
+ }
102
107
  if (dmScope === "per-channel-peer" && peerId) {
103
108
  const channel = (params.channel ?? "").trim().toLowerCase() || "unknown";
104
109
  return `agent:${normalizeAgentId(params.agentId)}:${channel}:dm:${peerId}`;
@@ -405,7 +405,7 @@ async function collectChannelSecurityFindings(params) {
405
405
  severity: "warn",
406
406
  title: `${input.label} DMs share the main session`,
407
407
  detail: "Multiple DM senders currently share the main session, which can leak context across users.",
408
- remediation: 'Set session.dmScope="per-channel-peer" to isolate DM sessions per sender.',
408
+ remediation: 'Set session.dmScope="per-channel-peer" (or "per-account-channel-peer" for multi-account channels) to isolate DM sessions per sender.',
409
409
  });
410
410
  }
411
411
  };
@@ -27,11 +27,13 @@ export async function maybeBroadcastMessage(params) {
27
27
  sessionKey: buildAgentSessionKey({
28
28
  agentId: normalizedAgentId,
29
29
  channel: "whatsapp",
30
+ accountId: params.route.accountId,
30
31
  peer: {
31
32
  kind: params.msg.chatType === "group" ? "group" : "dm",
32
33
  id: params.peerId,
33
34
  },
34
35
  dmScope: params.cfg.session?.dmScope,
36
+ identityLinks: params.cfg.session?.identityLinks,
35
37
  }),
36
38
  mainSessionKey: buildAgentMainSessionKey({
37
39
  agentId: normalizedAgentId,
@@ -20,5 +20,5 @@ moltbot security audit --deep
20
20
  moltbot security audit --fix
21
21
  ```
22
22
 
23
- The audit warns when multiple DM senders share the main session and recommends `session.dmScope="per-channel-peer"` for shared inboxes.
23
+ The audit warns when multiple DM senders share the main session and recommends `session.dmScope="per-channel-peer"` (or `per-account-channel-peer` for multi-account channels) for shared inboxes.
24
24
  It also warns when small models (`<=300B`) are used without sandboxing and with web/browser tools enabled.
@@ -11,7 +11,8 @@ Use `session.dmScope` to control how **direct messages** are grouped:
11
11
  - `main` (default): all DMs share the main session for continuity.
12
12
  - `per-peer`: isolate by sender id across channels.
13
13
  - `per-channel-peer`: isolate by channel + sender (recommended for multi-user inboxes).
14
- Use `session.identityLinks` to map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when using `per-peer` or `per-channel-peer`.
14
+ - `per-account-channel-peer`: isolate by account + channel + sender (recommended for multi-account inboxes).
15
+ Use `session.identityLinks` to map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when using `per-peer`, `per-channel-peer`, or `per-account-channel-peer`.
15
16
 
16
17
  ## Gateway is the source of truth
17
18
  All session state is **owned by the gateway** (the “master” Moltbot). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files.
@@ -44,6 +45,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
44
45
  - Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
45
46
  - `per-peer`: `agent:<agentId>:dm:<peerId>`.
46
47
  - `per-channel-peer`: `agent:<agentId>:<channel>:dm:<peerId>`.
48
+ - `per-account-channel-peer`: `agent:<agentId>:<channel>:<accountId>:dm:<peerId>` (accountId defaults to `default`).
47
49
  - If `session.identityLinks` matches a provider-prefixed peer id (for example `telegram:123`), the canonical key replaces `<peerId>` so the same person shares a session across channels.
48
50
  - Group chats isolate state: `agent:<agentId>:<channel>:group:<id>` (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
49
51
  - Telegram forum topics append `:topic:<threadId>` to the group id for isolation.
@@ -94,7 +96,7 @@ Send these as standalone messages so they register.
94
96
  {
95
97
  session: {
96
98
  scope: "per-sender", // keep group keys separate
97
- dmScope: "main", // DM continuity (set per-channel-peer for shared inboxes)
99
+ dmScope: "main", // DM continuity (set per-channel-peer/per-account-channel-peer for shared inboxes)
98
100
  identityLinks: {
99
101
  alice: ["telegram:123456789", "discord:987654321012345678"]
100
102
  },
@@ -2657,7 +2657,8 @@ Fields:
2657
2657
  - `main`: all DMs share the main session for continuity.
2658
2658
  - `per-peer`: isolate DMs by sender id across channels.
2659
2659
  - `per-channel-peer`: isolate DMs per channel + sender (recommended for multi-user inboxes).
2660
- - `identityLinks`: map canonical ids to provider-prefixed peers so the same person shares a DM session across channels when using `per-peer` or `per-channel-peer`.
2660
+ - `per-account-channel-peer`: isolate DMs per account + channel + sender (recommended for multi-account inboxes).
2661
+ - `identityLinks`: map canonical ids to provider-prefixed peers so the same person shares a DM session across channels when using `per-peer`, `per-channel-peer`, or `per-account-channel-peer`.
2661
2662
  - Example: `alice: ["telegram:123456789", "discord:987654321012345678"]`.
2662
2663
  - `reset`: primary reset policy. Defaults to daily resets at 4:00 AM local time on the gateway host.
2663
2664
  - `mode`: `daily` or `idle` (default: `daily` when `reset` is present).
@@ -199,7 +199,7 @@ By default, Moltbot routes **all DMs into the main session** so your assistant h
199
199
  }
200
200
  ```
201
201
 
202
- This prevents cross-user context leakage while keeping group chats isolated. If the same person contacts you on multiple channels, use `session.identityLinks` to collapse those DM sessions into one canonical identity. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration).
202
+ This prevents cross-user context leakage while keeping group chats isolated. If you run multiple accounts on the same channel, use `per-account-channel-peer` instead. If the same person contacts you on multiple channels, use `session.identityLinks` to collapse those DM sessions into one canonical identity. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration).
203
203
 
204
204
  ## Allowlists (DM + groups) — terminology
205
205
 
@@ -0,0 +1 @@
1
+ molt for termux.
@@ -0,0 +1,69 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Updating packages..."
5
+ pkg update -y && pkg upgrade -y
6
+
7
+ echo "Installing Node.js (LTS)..."
8
+ pkg install nodejs-lts -y
9
+
10
+ if ! command -v pnpm &> /dev/null; then
11
+ echo "Installing pnpm via npm..."
12
+ npm install -g pnpm
13
+
14
+ echo "Setting up pnpm..."
15
+ pnpm setup
16
+ else
17
+ echo "pnpm is already installed, skipping installation."
18
+ fi
19
+
20
+ # Reload PATH for current session
21
+ export PNPM_HOME="$HOME/.local/share/pnpm"
22
+ case ":$PATH:" in
23
+ *":$PNPM_HOME:"*) ;;
24
+ *) export PATH="$PNPM_HOME:$PATH" ;;
25
+ esac
26
+
27
+ echo "Installing moltbot-termux globally..."
28
+ pnpm add -g moltbot-termux
29
+
30
+ echo "Applying Termux clipboard fix to pnpm store..."
31
+
32
+ # Find the clipboard package index.js in the pnpm global store.
33
+ # This searches for the specific file we need to stub out.
34
+ CLIPBOARD_FIX_PATH=$(find "$HOME/.local/share/pnpm/global" -name "index.js" -path "*/@mariozechner/clipboard/*" | head -n 1)
35
+
36
+ if [ -n "$CLIPBOARD_FIX_PATH" ]; then
37
+ echo "Found clipboard package at: $CLIPBOARD_FIX_PATH"
38
+ cat > "$CLIPBOARD_FIX_PATH" <<EOF
39
+ module.exports = {
40
+ availableFormats: () => [],
41
+ getText: () => "",
42
+ setText: () => {},
43
+ hasText: () => false,
44
+ getImageBinary: () => null,
45
+ getImageBase64: () => null,
46
+ setImageBinary: () => {},
47
+ setImageBase64: () => {},
48
+ hasImage: () => false,
49
+ getHtml: () => "",
50
+ setHtml: () => {},
51
+ hasHtml: () => false,
52
+ getRtf: () => "",
53
+ setRtf: () => {},
54
+ hasRtf: () => false,
55
+ clear: () => {},
56
+ watch: () => {},
57
+ callThreadsafeFunction: () => {}
58
+ };
59
+ EOF
60
+ echo "Clipboard fix applied successfully."
61
+ else
62
+ echo "Warning: Could not automatically locate @mariozechner/clipboard in the pnpm store."
63
+ echo "You may need to apply the fix manually if you see native binding errors."
64
+ fi
65
+
66
+ echo ""
67
+ echo "Installation complete!"
68
+ echo "Please restart your terminal or run: source ~/.bashrc (or your shell config)"
69
+ echo "Then run 'moltbot onboard' to get started."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moltbot-termux",
3
- "version": "2026.1.28-3-pre",
3
+ "version": "2026.1.28-4",
4
4
  "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,101 +0,0 @@
1
- ---
2
- name: bitwarden
3
- description: Manage passwords and credentials via Bitwarden CLI (bw). Use for storing, retrieving, creating, or updating logins, credit cards, secure notes, and identities. Trigger when automating authentication, filling payment forms, or managing secrets programmatically.
4
- ---
5
-
6
- # Bitwarden CLI
7
-
8
- Full read/write vault access via `bw` command.
9
-
10
- ## Prerequisites
11
-
12
- ```bash
13
- brew install bitwarden-cli
14
- bw login <email> # one-time, prompts for master password
15
- ```
16
-
17
- ## Session Management
18
-
19
- Bitwarden requires an unlocked session. Use the helper script:
20
-
21
- ```bash
22
- source scripts/bw-session.sh <master_password>
23
- # Sets BW_SESSION env var
24
- ```
25
-
26
- Or manually:
27
- ```bash
28
- export BW_SESSION=$(echo '<password>' | bw unlock --raw)
29
- bw sync # always sync after unlock
30
- ```
31
-
32
- ## Common Operations
33
-
34
- ### Retrieve credentials
35
- ```bash
36
- bw get password "Site Name"
37
- bw get username "Site Name"
38
- bw get item "Site Name" --pretty | jq '.login'
39
- ```
40
-
41
- ### Create login
42
- ```bash
43
- bw get template item | jq '
44
- .type = 1 |
45
- .name = "Site Name" |
46
- .login.username = "user@email.com" |
47
- .login.password = "secret123" |
48
- .login.uris = [{uri: "https://example.com"}]
49
- ' | bw encode | bw create item
50
- ```
51
-
52
- ### Create credit card
53
- ```bash
54
- bw get template item | jq '
55
- .type = 3 |
56
- .name = "Card Name" |
57
- .card.cardholderName = "John Doe" |
58
- .card.brand = "Visa" |
59
- .card.number = "4111111111111111" |
60
- .card.expMonth = "12" |
61
- .card.expYear = "2030" |
62
- .card.code = "123"
63
- ' | bw encode | bw create item
64
- ```
65
-
66
- ### Get card for payment automation
67
- ```bash
68
- bw get item "Card Name" | jq -r '.card | "\(.number) \(.expMonth)/\(.expYear) \(.code)"'
69
- ```
70
-
71
- ### List items
72
- ```bash
73
- bw list items | jq -r '.[] | "\(.type)|\(.name)"'
74
- # Types: 1=login, 2=note, 3=card, 4=identity
75
- ```
76
-
77
- ### Search
78
- ```bash
79
- bw list items --search "vilaviniteca" | jq '.[0]'
80
- ```
81
-
82
- ## Item Types
83
-
84
- | Type | Value | Use |
85
- |------|-------|-----|
86
- | Login | 1 | Website credentials |
87
- | Secure Note | 2 | Freeform text |
88
- | Card | 3 | Credit/debit cards |
89
- | Identity | 4 | Personal info |
90
-
91
- ## References
92
-
93
- - [templates.md](references/templates.md) — Full jq templates for all item types
94
- - [Bitwarden CLI docs](https://bitwarden.com/help/cli/)
95
-
96
- ## Tips
97
-
98
- 1. **Always sync** after creating/editing items: `bw sync`
99
- 2. **Session expires** — re-unlock if you get auth errors
100
- 3. **Delete sensitive messages** after receiving credentials
101
- 4. **Card numbers** may not import from other managers (security restriction)
@@ -1,116 +0,0 @@
1
- # Bitwarden Item Templates
2
-
3
- jq patterns for creating vault items via CLI.
4
-
5
- ## Login (type=1)
6
-
7
- ```bash
8
- bw get template item | jq '
9
- .type = 1 |
10
- .name = "Example Site" |
11
- .notes = "Optional notes" |
12
- .favorite = false |
13
- .login.username = "user@example.com" |
14
- .login.password = "secretPassword123" |
15
- .login.totp = "otpauth://totp/..." |
16
- .login.uris = [
17
- {uri: "https://example.com", match: null},
18
- {uri: "https://app.example.com", match: null}
19
- ]
20
- ' | bw encode | bw create item
21
- ```
22
-
23
- ## Credit Card (type=3)
24
-
25
- ```bash
26
- bw get template item | jq '
27
- .type = 3 |
28
- .name = "Visa ending 1234" |
29
- .notes = "Primary card" |
30
- .card.cardholderName = "JOHN DOE" |
31
- .card.brand = "Visa" |
32
- .card.number = "4111111111111111" |
33
- .card.expMonth = "12" |
34
- .card.expYear = "2030" |
35
- .card.code = "123"
36
- ' | bw encode | bw create item
37
- ```
38
-
39
- **Brands:** Visa, Mastercard, Amex, Discover, Diners Club, JCB, Maestro, UnionPay, Other
40
-
41
- ## Secure Note (type=2)
42
-
43
- ```bash
44
- bw get template item | jq '
45
- .type = 2 |
46
- .name = "API Keys" |
47
- .notes = "OPENAI_KEY=sk-xxx\nANTHROPIC_KEY=sk-ant-xxx" |
48
- .secureNote.type = 0
49
- ' | bw encode | bw create item
50
- ```
51
-
52
- ## Identity (type=4)
53
-
54
- ```bash
55
- bw get template item | jq '
56
- .type = 4 |
57
- .name = "Personal Info" |
58
- .identity.title = "Mr" |
59
- .identity.firstName = "John" |
60
- .identity.lastName = "Doe" |
61
- .identity.email = "john@example.com" |
62
- .identity.phone = "+34612345678" |
63
- .identity.address1 = "123 Main St" |
64
- .identity.city = "Barcelona" |
65
- .identity.state = "Catalunya" |
66
- .identity.postalCode = "08001" |
67
- .identity.country = "ES"
68
- ' | bw encode | bw create item
69
- ```
70
-
71
- ## Edit Existing Item
72
-
73
- ```bash
74
- # Get item, modify, update
75
- bw get item <id> | jq '.login.password = "newPassword"' | bw encode | bw edit item <id>
76
- ```
77
-
78
- ## Custom Fields
79
-
80
- ```bash
81
- bw get template item | jq '
82
- .type = 1 |
83
- .name = "With Custom Fields" |
84
- .fields = [
85
- {name: "Security Question", value: "Pet name", type: 0},
86
- {name: "PIN", value: "1234", type: 1}
87
- ]
88
- ' | bw encode | bw create item
89
- ```
90
-
91
- **Field types:** 0=text, 1=hidden, 2=boolean
92
-
93
- ## Retrieve Patterns
94
-
95
- ```bash
96
- # Password only
97
- bw get password "Site Name"
98
-
99
- # Username only
100
- bw get username "Site Name"
101
-
102
- # Full login object
103
- bw get item "Site Name" | jq '.login'
104
-
105
- # Card number
106
- bw get item "Card Name" | jq -r '.card.number'
107
-
108
- # All card fields for form filling
109
- bw get item "Card Name" | jq -r '.card | [.number, .expMonth, .expYear, .code] | @tsv'
110
-
111
- # Search by URL
112
- bw list items --url "example.com" | jq '.[0].login'
113
-
114
- # List all cards
115
- bw list items | jq '.[] | select(.type == 3) | .name'
116
- ```
@@ -1,33 +0,0 @@
1
- #!/bin/bash
2
- # Unlock Bitwarden vault and export session key
3
- # Usage: source bw-session.sh <master_password>
4
- # Or: source bw-session.sh (prompts for password)
5
-
6
- set -e
7
-
8
- if [ -n "$1" ]; then
9
- MASTER_PW="$1"
10
- else
11
- read -sp "Bitwarden master password: " MASTER_PW
12
- echo
13
- fi
14
-
15
- # Check if already logged in
16
- if ! bw login --check &>/dev/null; then
17
- echo "Not logged in. Run: bw login <email>"
18
- return 1
19
- fi
20
-
21
- # Unlock and get session
22
- export BW_SESSION=$(echo "$MASTER_PW" | bw unlock --raw 2>/dev/null)
23
-
24
- if [ -z "$BW_SESSION" ]; then
25
- echo "Failed to unlock vault"
26
- return 1
27
- fi
28
-
29
- # Sync to get latest
30
- bw sync &>/dev/null
31
-
32
- echo "✓ Vault unlocked and synced"
33
- echo "Session valid for this shell"