moltbot-termux 2026.1.27-2-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 +4 -0
- package/README.md +53 -4
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/cli/banner.js +1 -0
- package/dist/cli/program/register.subclis.js +5 -0
- package/dist/commands/doctor-security.js +1 -1
- package/dist/commands/onboard-channels.js +2 -2
- package/dist/commands/onboard-helpers.js +2 -1
- package/dist/config/schema.js +1 -1
- package/dist/config/zod-schema.session.js +6 -1
- package/dist/discord/send.shared.js +6 -4
- package/dist/discord/targets.js +17 -4
- package/dist/hooks/bundled/session-memory/HOOK.md +25 -2
- package/dist/hooks/bundled/session-memory/handler.js +17 -10
- package/dist/infra/outbound/outbound-session.js +18 -0
- package/dist/media/mime.js +3 -0
- package/dist/routing/resolve-route.js +2 -0
- package/dist/routing/session-key.js +5 -0
- package/dist/security/audit.js +1 -1
- package/dist/web/auto-reply/monitor/broadcast.js +2 -0
- package/docs/cli/security.md +1 -1
- package/docs/concepts/session.md +4 -2
- package/docs/gateway/configuration.md +2 -1
- package/docs/gateway/security/index.md +1 -1
- package/docs/index.html +1 -0
- package/docs/install.sh +69 -0
- package/package.json +4 -3
- package/skills/bitwarden/SKILL.md +0 -101
- package/skills/bitwarden/references/templates.md +0 -116
- package/skills/bitwarden/scripts/bw-session.sh +0 -33
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
|
|
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
|
|
117
|
+
moltbot onboard
|
|
69
118
|
|
|
70
119
|
moltbot gateway --port 18789 --verbose
|
|
71
120
|
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1ebedd0b9b5770fc0a27d08fa3c730f4c5746893745477e92468598a63777554
|
package/dist/cli/banner.js
CHANGED
|
@@ -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
|
}
|
package/dist/config/schema.js
CHANGED
|
@@ -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([
|
|
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
|
}
|
package/dist/discord/targets.js
CHANGED
|
@@ -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 -
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
20
|
-
const
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
package/dist/media/mime.js
CHANGED
|
@@ -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}`;
|
package/dist/security/audit.js
CHANGED
|
@@ -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,
|
package/docs/cli/security.md
CHANGED
|
@@ -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.
|
package/docs/concepts/session.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
- `
|
|
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
|
|
package/docs/index.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
molt for termux.
|
package/docs/install.sh
ADDED
|
@@ -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.
|
|
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",
|
|
@@ -242,7 +242,8 @@
|
|
|
242
242
|
"wireit": "^0.14.12"
|
|
243
243
|
},
|
|
244
244
|
"overrides": {
|
|
245
|
-
"tar": "7.5.4"
|
|
245
|
+
"tar": "7.5.4",
|
|
246
|
+
"@mariozechner/clipboard": "npm:noop-package@1.0.0"
|
|
246
247
|
},
|
|
247
248
|
"pnpm": {
|
|
248
249
|
"minimumReleaseAge": 2880,
|
|
@@ -250,7 +251,7 @@
|
|
|
250
251
|
"@sinclair/typebox": "0.34.47",
|
|
251
252
|
"hono": "4.11.4",
|
|
252
253
|
"tar": "7.5.4",
|
|
253
|
-
"@mariozechner/clipboard": "
|
|
254
|
+
"@mariozechner/clipboard": "npm:noop-package@1.0.0"
|
|
254
255
|
}
|
|
255
256
|
},
|
|
256
257
|
"vitest": {
|
|
@@ -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"
|