agiagent-dev 2026.1.32 → 2026.1.33

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.
@@ -768,12 +768,16 @@ export function createExecTool(defaults) {
768
768
  // Fall back to requiring approval if node approvals cannot be fetched.
769
769
  }
770
770
  }
771
- const requiresAsk = requiresExecApproval({
772
- ask: hostAsk,
773
- security: hostSecurity,
774
- analysisOk,
775
- allowlistSatisfied,
776
- });
771
+ // In hosted mode, skip approval entirely - commands are pre-authorized by the gateway
772
+ const hostedAutoApprove = process.env.AGIAGENT_HOSTED_MODE === "1";
773
+ const requiresAsk = hostedAutoApprove
774
+ ? false
775
+ : requiresExecApproval({
776
+ ask: hostAsk,
777
+ security: hostSecurity,
778
+ analysisOk,
779
+ allowlistSatisfied,
780
+ });
777
781
  const commandText = params.command;
778
782
  const invokeTimeoutMs = Math.max(10_000, (typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec) * 1000 + 5_000);
779
783
  const buildInvokeParams = (approvedByAsk, approvalDecision, runId) => ({
@@ -782,7 +786,9 @@ export function createExecTool(defaults) {
782
786
  params: {
783
787
  command: argv,
784
788
  rawCommand: params.command,
785
- cwd: workdir,
789
+ // Don't pass gateway's cwd to remote nodes - use null to let node use its own cwd
790
+ // The gateway's cwd (e.g., /app or /data on Fly.io) doesn't exist on user's machine
791
+ cwd: params.workdir?.trim() || null,
786
792
  env: nodeEnv,
787
793
  timeoutMs: typeof params.timeout === "number" ? params.timeout * 1000 : undefined,
788
794
  agentId,
@@ -790,6 +796,8 @@ export function createExecTool(defaults) {
790
796
  approved: approvedByAsk,
791
797
  approvalDecision: approvalDecision ?? undefined,
792
798
  runId: runId ?? undefined,
799
+ // Tell node to skip macOS app exec host and use direct spawn in hosted mode
800
+ hostedMode: process.env.AGIAGENT_HOSTED_MODE === "1" ? true : undefined,
793
801
  },
794
802
  idempotencyKey: crypto.randomUUID(),
795
803
  });
@@ -893,7 +901,8 @@ export function createExecTool(defaults) {
893
901
  };
894
902
  }
895
903
  const startedAt = Date.now();
896
- const raw = await callGatewayTool("node.invoke", { timeoutMs: invokeTimeoutMs }, buildInvokeParams(false, null));
904
+ // In hosted mode, hostedAutoApprove is true, so we send approved=true to the node
905
+ const raw = await callGatewayTool("node.invoke", { timeoutMs: invokeTimeoutMs }, buildInvokeParams(hostedAutoApprove, hostedAutoApprove ? "allow-once" : null));
897
906
  const payload = raw && typeof raw === "object" ? raw.payload : undefined;
898
907
  const payloadObj = payload && typeof payload === "object" ? payload : {};
899
908
  const stdout = typeof payloadObj.stdout === "string" ? payloadObj.stdout : "";
@@ -314,6 +314,23 @@ export function buildAgentSystemPrompt(params) {
314
314
  "",
315
315
  ...skillsSection,
316
316
  ...memorySection,
317
+ // File type guidance for document handling
318
+ !isMinimal ? "## File Type Guidance" : "",
319
+ !isMinimal
320
+ ? [
321
+ "When working with specific file types, ALWAYS read the corresponding skill BEFORE any editing:",
322
+ "",
323
+ "### Word Documents (.docx)",
324
+ "1. Find the `docx` skill in <available_skills> above",
325
+ `2. Use \`${readToolName}\` to read the skill's SKILL.md at its <location>`,
326
+ "3. Follow ALL instructions in that skill - it contains CRITICAL rules",
327
+ "4. Use python-docx (NOT raw XML/zipfile manipulation)",
328
+ "5. Count paragraphs/bullets before AND after editing - counts must match unless user requested reduction",
329
+ "",
330
+ "NEVER skip reading the skill. NEVER use zipfile+ElementTree for .docx files.",
331
+ ].join("\n")
332
+ : "",
333
+ !isMinimal ? "" : "",
317
334
  // Skip self-update for subagent/none modes
318
335
  hasGateway && !isMinimal ? "## AGIAgent Self-Update" : "",
319
336
  hasGateway && !isMinimal
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.1.32",
3
- "commit": "63a5a6cdc28f72ec1bb0ec415ff148eba790b2d8",
4
- "builtAt": "2026-02-02T03:31:57.234Z"
2
+ "version": "2026.1.33",
3
+ "commit": "d2d5c683d8c0716e612985048828ecd36b48de8c",
4
+ "builtAt": "2026-02-02T18:19:42.158Z"
5
5
  }
@@ -1 +1 @@
1
- 0c8337264f78308ab09fce8a670fbfae17efd8cad8a60f1d46a50e9a48a5eec6
1
+ 48ae5a7be920c7ca3c20f3800f4b9c0a40fa52956e24f6fbf8ec668ac4b14679
@@ -44,7 +44,11 @@ ${theme.muted("Your WhatsApp messages will trigger AI that runs commands on this
44
44
  if (gatewayUrl.includes("://")) {
45
45
  const url = new URL(gatewayUrl);
46
46
  host = url.hostname;
47
- port = url.port ? parseInt(url.port, 10) : (url.protocol === "wss:" || url.protocol === "https:" ? 443 : 80);
47
+ port = url.port
48
+ ? parseInt(url.port, 10)
49
+ : url.protocol === "wss:" || url.protocol === "https:"
50
+ ? 443
51
+ : 80;
48
52
  useTls = url.protocol === "wss:" || url.protocol === "https:";
49
53
  }
50
54
  else if (gatewayUrl.includes(":")) {
@@ -1,11 +1,12 @@
1
1
  import { normalizeProviderId } from "../agents/model-selection.js";
2
2
  import { getChannelPluginCatalogEntry, listChannelPluginCatalogEntries, } from "../channels/plugins/catalog.js";
3
- import { getChatChannelMeta, listChatChannels, normalizeChatChannelId, } from "../channels/registry.js";
3
+ import { CHAT_CHANNEL_ORDER, getChatChannelMeta, normalizeChatChannelId, } from "../channels/registry.js";
4
4
  import { hasAnyWhatsAppAuth } from "../web/accounts.js";
5
- const CHANNEL_PLUGIN_IDS = Array.from(new Set([
6
- ...listChatChannels().map((meta) => meta.id),
7
- ...listChannelPluginCatalogEntries().map((entry) => entry.id),
8
- ]));
5
+ // Core/built-in channels are part of the main codebase and don't need plugin entries.
6
+ // Only extension channels (actual plugins) should be auto-enabled via plugins.entries.
7
+ const CORE_CHANNEL_IDS = new Set(CHAT_CHANNEL_ORDER);
8
+ // Extension channel plugins that can be auto-enabled
9
+ const CHANNEL_PLUGIN_IDS = Array.from(new Set([...listChannelPluginCatalogEntries().map((entry) => entry.id)]));
9
10
  const PROVIDER_PLUGIN_IDS = [
10
11
  { pluginId: "google-antigravity-auth", providerId: "google-antigravity" },
11
12
  { pluginId: "google-gemini-cli-auth", providerId: "google-gemini-cli" },
@@ -245,13 +246,20 @@ function resolveConfiguredPlugins(cfg, env) {
245
246
  if (key === "defaults") {
246
247
  continue;
247
248
  }
248
- channelIds.add(key);
249
+ // Only add extension channels, not core channels
250
+ if (!CORE_CHANNEL_IDS.has(key)) {
251
+ channelIds.add(key);
252
+ }
249
253
  }
250
254
  }
251
255
  for (const channelId of channelIds) {
252
256
  if (!channelId) {
253
257
  continue;
254
258
  }
259
+ // Skip core channels - they're built-in and don't need plugin entries
260
+ if (CORE_CHANNEL_IDS.has(channelId)) {
261
+ continue;
262
+ }
255
263
  if (isChannelConfigured(cfg, channelId, env)) {
256
264
  changes.push({
257
265
  pluginId: channelId,
@@ -19,6 +19,7 @@ export type GatewayAuthResult = {
19
19
  userName: string;
20
20
  tokenId: string;
21
21
  whatsappAccountId: string | null;
22
+ telegramBotToken: string | null;
22
23
  };
23
24
  };
24
25
  type ConnectAuth = {
@@ -1,7 +1,7 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
2
  import { readTailscaleWhoisIdentity } from "../infra/tailscale.js";
3
- import { isTrustedProxyAddress, parseForwardedForClientIp, resolveGatewayClientIp } from "./net.js";
4
3
  import { isHostedMode, validateHostedToken } from "./hosted-db.js";
4
+ import { isTrustedProxyAddress, parseForwardedForClientIp, resolveGatewayClientIp } from "./net.js";
5
5
  function safeEqual(a, b) {
6
6
  if (a.length !== b.length) {
7
7
  return false;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Hosted mode AI agent processing.
3
+ *
4
+ * Handles running the AI agent for hosted users using the existing
5
+ * auto-reply infrastructure with full tool support.
6
+ */
7
+ export type HostedAgentReplyParams = {
8
+ userId: string;
9
+ userName: string;
10
+ nodeId: string;
11
+ messageText: string;
12
+ senderJid: string;
13
+ chatJid: string;
14
+ isGroup: boolean;
15
+ sendNodeEvent: (event: string, payload: unknown) => void;
16
+ };
17
+ /**
18
+ * Process a message through the AI agent for a hosted user.
19
+ * Uses the full agent infrastructure with tool support.
20
+ */
21
+ export declare function runHostedAgentReply(params: HostedAgentReplyParams): Promise<string>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Hosted mode AI agent processing.
3
+ *
4
+ * Handles running the AI agent for hosted users using the existing
5
+ * auto-reply infrastructure with full tool support.
6
+ */
7
+ import { getReplyFromConfig } from "../auto-reply/reply.js";
8
+ import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
9
+ import { loadConfig } from "../config/config.js";
10
+ import { createSubsystemLogger } from "../logging/subsystem.js";
11
+ import { jidToE164 } from "../utils.js";
12
+ const log = createSubsystemLogger("hosted-agent");
13
+ /**
14
+ * Process a message through the AI agent for a hosted user.
15
+ * Uses the full agent infrastructure with tool support.
16
+ */
17
+ export async function runHostedAgentReply(params) {
18
+ const { userId, userName, nodeId, messageText, senderJid, chatJid, isGroup, sendNodeEvent } = params;
19
+ log.info(`Processing message for user ${userName}: "${messageText.slice(0, 50)}..."`);
20
+ try {
21
+ const cfg = loadConfig();
22
+ // Create a per-user session key to isolate conversations between hosted users
23
+ // Format: hosted:<userId>:<chatJid> for full isolation
24
+ const userSessionKey = `hosted:${userId}:${chatJid}`;
25
+ const senderE164 = jidToE164(senderJid) ?? senderJid;
26
+ log.info(`Routing to session ${userSessionKey} for user ${userName}`);
27
+ // Build the message context using the existing infrastructure
28
+ const ctx = finalizeInboundContext({
29
+ Body: messageText,
30
+ RawBody: messageText,
31
+ CommandBody: messageText,
32
+ BodyForAgent: messageText,
33
+ BodyForCommands: messageText,
34
+ From: senderJid,
35
+ To: userId,
36
+ SessionKey: userSessionKey,
37
+ AccountId: userId,
38
+ MessageSid: `hosted-${Date.now()}`,
39
+ ChatType: isGroup ? "group" : "dm",
40
+ ConversationLabel: chatJid,
41
+ SenderName: userName,
42
+ SenderId: senderJid,
43
+ SenderE164: senderE164,
44
+ // Allow commands for hosted users
45
+ CommandAuthorized: true,
46
+ Provider: "telegram",
47
+ Surface: "telegram",
48
+ OriginatingChannel: "telegram",
49
+ OriginatingTo: chatJid,
50
+ });
51
+ // Create config override to bind exec to the user's connected node
52
+ // This ensures commands run on the user's device, not on the gateway
53
+ const configOverride = {
54
+ ...cfg,
55
+ tools: {
56
+ ...cfg.tools,
57
+ exec: {
58
+ ...cfg.tools?.exec,
59
+ host: "node",
60
+ node: nodeId,
61
+ },
62
+ },
63
+ };
64
+ // Get the AI reply using the full agent infrastructure
65
+ // Pass the config override so exec is bound to the user's node
66
+ const result = await getReplyFromConfig(ctx, undefined, configOverride);
67
+ if (!result) {
68
+ log.warn(`No reply generated for user ${userName}`);
69
+ return "";
70
+ }
71
+ // Extract text from reply payload(s)
72
+ const replyText = extractReplyText(result);
73
+ log.info(`Generated reply for user ${userName}: "${replyText.slice(0, 50)}..."`);
74
+ return replyText;
75
+ }
76
+ catch (err) {
77
+ log.error(`Failed to generate reply for user ${userName}`, { error: String(err) });
78
+ sendNodeEvent("agent.error", {
79
+ userId,
80
+ error: String(err),
81
+ });
82
+ return `Sorry, I encountered an error: ${String(err).slice(0, 100)}`;
83
+ }
84
+ }
85
+ /**
86
+ * Extract text from reply payload(s).
87
+ */
88
+ function extractReplyText(result) {
89
+ if (Array.isArray(result)) {
90
+ return result
91
+ .map((r) => r.text ?? "")
92
+ .filter(Boolean)
93
+ .join("\n");
94
+ }
95
+ return result.text ?? "";
96
+ }
@@ -12,6 +12,7 @@ export type ConnectionToken = {
12
12
  userId: string;
13
13
  token: string;
14
14
  whatsappAccountId: string | null;
15
+ telegramBotToken: string | null;
15
16
  createdAt: Date;
16
17
  lastConnectedAt: Date | null;
17
18
  };
@@ -52,6 +53,7 @@ export declare function validateHostedToken(token: string): Promise<{
52
53
  userName: string;
53
54
  tokenId: string;
54
55
  whatsappAccountId: string | null;
56
+ telegramBotToken: string | null;
55
57
  } | null>;
56
58
  /**
57
59
  * Register a node for a hosted user.
@@ -44,6 +44,7 @@ export async function initHostedDb() {
44
44
  t.user_id,
45
45
  t.token,
46
46
  t.whatsapp_account_id,
47
+ t.telegram_bot_token,
47
48
  t.created_at as token_created_at,
48
49
  t.last_connected_at,
49
50
  u.id as user_id,
@@ -69,6 +70,7 @@ export async function initHostedDb() {
69
70
  userId: row.user_id,
70
71
  token: row.token,
71
72
  whatsappAccountId: row.whatsapp_account_id,
73
+ telegramBotToken: row.telegram_bot_token,
72
74
  createdAt: row.token_created_at,
73
75
  lastConnectedAt: row.last_connected_at,
74
76
  },
@@ -128,6 +130,7 @@ export async function validateHostedToken(token) {
128
130
  userName: result.user.name,
129
131
  tokenId: result.tokenRecord.id,
130
132
  whatsappAccountId: result.tokenRecord.whatsappAccountId,
133
+ telegramBotToken: result.tokenRecord.telegramBotToken,
131
134
  };
132
135
  }
133
136
  // In-memory mapping of userId -> nodeId for WhatsApp event routing
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Hosted mode Telegram handling.
3
+ *
4
+ * Manages Telegram bots for hosted users:
5
+ * 1. Starts Telegram bot with user's token
6
+ * 2. Handles incoming messages
7
+ * 3. Routes to AI agent
8
+ * 4. Sends responses
9
+ */
10
+ /**
11
+ * Start a Telegram bot for a hosted user.
12
+ */
13
+ export declare function startHostedTelegramBot(params: {
14
+ userId: string;
15
+ userName: string;
16
+ nodeId: string;
17
+ botToken: string;
18
+ sendEvent: (event: string, payload: unknown) => void;
19
+ }): Promise<void>;
20
+ /**
21
+ * Stop a Telegram bot for a hosted user.
22
+ */
23
+ export declare function stopHostedTelegramBot(userId: string): Promise<void>;
24
+ /**
25
+ * Check if a hosted user has an active Telegram bot.
26
+ */
27
+ export declare function isHostedTelegramBotRunning(userId: string): boolean;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Hosted mode Telegram handling.
3
+ *
4
+ * Manages Telegram bots for hosted users:
5
+ * 1. Starts Telegram bot with user's token
6
+ * 2. Handles incoming messages
7
+ * 3. Routes to AI agent
8
+ * 4. Sends responses
9
+ */
10
+ import { Bot } from "grammy";
11
+ import { createSubsystemLogger } from "../logging/subsystem.js";
12
+ import { runHostedAgentReply } from "./hosted-agent.js";
13
+ import { isHostedMode } from "./hosted-db.js";
14
+ const log = createSubsystemLogger("hosted-telegram");
15
+ const activeHostedBots = new Map();
16
+ /**
17
+ * Start a Telegram bot for a hosted user.
18
+ */
19
+ export async function startHostedTelegramBot(params) {
20
+ if (!isHostedMode()) {
21
+ return;
22
+ }
23
+ const { userId, userName, nodeId, botToken, sendEvent } = params;
24
+ log.info(`Starting Telegram bot for hosted user ${userName} (${userId})`);
25
+ // Check if already has an active bot
26
+ const existing = activeHostedBots.get(userId);
27
+ if (existing?.running) {
28
+ log.info(`User ${userName} already has an active Telegram bot`);
29
+ // Update node reference
30
+ existing.nodeId = nodeId;
31
+ existing.sendNodeEvent = sendEvent;
32
+ sendEvent("telegram.connected", {
33
+ userId,
34
+ message: "Telegram bot already running",
35
+ });
36
+ return;
37
+ }
38
+ try {
39
+ // Create the bot
40
+ const bot = new Bot(botToken);
41
+ const session = {
42
+ bot,
43
+ running: false,
44
+ startedAt: Date.now(),
45
+ userId,
46
+ userName,
47
+ nodeId,
48
+ sendNodeEvent: sendEvent,
49
+ };
50
+ // Set up message handler
51
+ bot.on("message:text", async (ctx) => {
52
+ const text = ctx.message.text;
53
+ const chatId = ctx.chat.id;
54
+ const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
55
+ const senderId = ctx.from?.id?.toString() ?? "unknown";
56
+ const senderName = ctx.from?.first_name ?? ctx.from?.username ?? "User";
57
+ log.info(`Telegram message from ${senderName} for user ${userName}: ${text.slice(0, 50)}...`);
58
+ // Notify node
59
+ session.sendNodeEvent("telegram.message.received", {
60
+ userId,
61
+ from: senderId,
62
+ text: text.slice(0, 100),
63
+ isGroup,
64
+ });
65
+ try {
66
+ // Send typing indicator
67
+ await ctx.api.sendChatAction(chatId, "typing");
68
+ // Process through AI agent
69
+ const reply = await runHostedAgentReply({
70
+ userId,
71
+ userName,
72
+ nodeId: session.nodeId,
73
+ messageText: text,
74
+ senderJid: senderId,
75
+ chatJid: chatId.toString(),
76
+ isGroup,
77
+ sendNodeEvent: session.sendNodeEvent,
78
+ });
79
+ if (reply && reply.trim()) {
80
+ // Send reply
81
+ await ctx.reply(reply, { parse_mode: "HTML" }).catch(async () => {
82
+ // Fallback to plain text if HTML fails
83
+ await ctx.reply(reply);
84
+ });
85
+ log.info(`Telegram reply sent to ${chatId} for user ${userName}`);
86
+ session.sendNodeEvent("telegram.message.sent", {
87
+ userId,
88
+ to: chatId,
89
+ text: reply.slice(0, 100),
90
+ });
91
+ }
92
+ }
93
+ catch (err) {
94
+ log.error(`Failed to process Telegram message for user ${userName}`, {
95
+ error: String(err),
96
+ });
97
+ session.sendNodeEvent("telegram.message.error", {
98
+ userId,
99
+ error: String(err),
100
+ });
101
+ // Send error message to user
102
+ try {
103
+ await ctx.reply(`Sorry, I encountered an error: ${String(err).slice(0, 100)}`);
104
+ }
105
+ catch {
106
+ // Ignore send errors
107
+ }
108
+ }
109
+ });
110
+ // Handle errors
111
+ bot.catch((err) => {
112
+ log.error(`Telegram bot error for user ${userName}`, { error: String(err.message) });
113
+ });
114
+ // Store session
115
+ activeHostedBots.set(userId, session);
116
+ // Start the bot (polling mode)
117
+ log.info(`Starting Telegram polling for user ${userName}`);
118
+ // Get bot info first
119
+ const me = await bot.api.getMe();
120
+ log.info(`Telegram bot @${me.username} started for user ${userName}`);
121
+ // Start polling in background
122
+ bot.start({
123
+ onStart: () => {
124
+ session.running = true;
125
+ log.info(`Telegram bot polling started for user ${userName}`);
126
+ sendEvent("telegram.connected", {
127
+ userId,
128
+ botUsername: me.username,
129
+ message: `Telegram bot @${me.username} is ready! Send a message to the bot.`,
130
+ });
131
+ },
132
+ });
133
+ }
134
+ catch (err) {
135
+ log.error(`Failed to start Telegram bot for user ${userName}`, { error: String(err) });
136
+ sendEvent("telegram.error", {
137
+ userId,
138
+ error: `Failed to start Telegram bot: ${String(err)}`,
139
+ });
140
+ }
141
+ }
142
+ /**
143
+ * Stop a Telegram bot for a hosted user.
144
+ */
145
+ export async function stopHostedTelegramBot(userId) {
146
+ const session = activeHostedBots.get(userId);
147
+ if (session?.bot) {
148
+ try {
149
+ await session.bot.stop();
150
+ }
151
+ catch {
152
+ // Ignore stop errors
153
+ }
154
+ }
155
+ activeHostedBots.delete(userId);
156
+ }
157
+ /**
158
+ * Check if a hosted user has an active Telegram bot.
159
+ */
160
+ export function isHostedTelegramBotRunning(userId) {
161
+ const session = activeHostedBots.get(userId);
162
+ return session?.running ?? false;
163
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Hosted mode WhatsApp handling.
3
+ *
4
+ * When a hosted user's node connects, this module handles:
5
+ * 1. Starting a WhatsApp session for the user
6
+ * 2. Sending QR code events to their connected node
7
+ * 3. Monitoring incoming messages
8
+ * 4. Processing messages through the AI agent
9
+ * 5. Sending responses back via WhatsApp
10
+ */
11
+ import { createWaSocket } from "../web/session.js";
12
+ type HostedSession = {
13
+ sock: Awaited<ReturnType<typeof createWaSocket>> | null;
14
+ connected: boolean;
15
+ startedAt: number;
16
+ monitoring: boolean;
17
+ nodeId: string;
18
+ userId: string;
19
+ userName: string;
20
+ sendNodeEvent: (event: string, payload: unknown) => void;
21
+ };
22
+ /**
23
+ * Start WhatsApp for a hosted user - handles pairing and message monitoring.
24
+ * Called when a hosted user's node connects to the gateway.
25
+ */
26
+ export declare function startHostedWhatsAppPairing(params: {
27
+ userId: string;
28
+ userName: string;
29
+ nodeId: string;
30
+ sendEvent: (event: string, payload: unknown) => void;
31
+ }): Promise<void>;
32
+ /**
33
+ * Check if a hosted user has WhatsApp connected.
34
+ */
35
+ export declare function isHostedUserWhatsAppConnected(userId: string): Promise<boolean>;
36
+ /**
37
+ * Get an active session for a hosted user.
38
+ */
39
+ export declare function getHostedSession(userId: string): HostedSession | undefined;
40
+ /**
41
+ * Clean up WhatsApp session for a hosted user.
42
+ */
43
+ export declare function cleanupHostedWhatsAppSession(userId: string): void;
44
+ export {};
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Hosted mode WhatsApp handling.
3
+ *
4
+ * When a hosted user's node connects, this module handles:
5
+ * 1. Starting a WhatsApp session for the user
6
+ * 2. Sending QR code events to their connected node
7
+ * 3. Monitoring incoming messages
8
+ * 4. Processing messages through the AI agent
9
+ * 5. Sending responses back via WhatsApp
10
+ */
11
+ import { createSubsystemLogger } from "../logging/subsystem.js";
12
+ import { resolveHostedWhatsAppAuthDir } from "../web/accounts.js";
13
+ import { extractText } from "../web/inbound/extract.js";
14
+ import { sendMessageWhatsApp } from "../web/outbound.js";
15
+ import { createWaSocket, webAuthExists, readWebSelfId } from "../web/session.js";
16
+ import { runHostedAgentReply } from "./hosted-agent.js";
17
+ import { isHostedMode } from "./hosted-db.js";
18
+ const log = createSubsystemLogger("hosted-whatsapp");
19
+ const activeHostedSessions = new Map();
20
+ /**
21
+ * Start WhatsApp for a hosted user - handles pairing and message monitoring.
22
+ * Called when a hosted user's node connects to the gateway.
23
+ */
24
+ export async function startHostedWhatsAppPairing(params) {
25
+ if (!isHostedMode()) {
26
+ return;
27
+ }
28
+ const { userId, userName, nodeId, sendEvent } = params;
29
+ const authDir = resolveHostedWhatsAppAuthDir(userId);
30
+ log.info(`Starting WhatsApp for hosted user ${userName} (${userId})`);
31
+ // Check if already has an active session
32
+ const existing = activeHostedSessions.get(userId);
33
+ if (existing?.sock && existing.connected) {
34
+ log.info(`User ${userName} already has an active WhatsApp session`);
35
+ sendEvent("whatsapp.connected", {
36
+ userId,
37
+ message: "WhatsApp session already active",
38
+ });
39
+ // Update node reference in case of reconnection
40
+ existing.nodeId = nodeId;
41
+ existing.sendNodeEvent = sendEvent;
42
+ return;
43
+ }
44
+ // Check if has stored auth (already linked)
45
+ const hasAuth = await webAuthExists(authDir);
46
+ const selfId = hasAuth ? readWebSelfId(authDir) : { jid: null, e164: null };
47
+ // Start or resume WhatsApp connection
48
+ try {
49
+ log.info(`Creating WhatsApp socket for user ${userName}`);
50
+ const sock = await createWaSocket(false, false, {
51
+ authDir,
52
+ onQr: (qr) => {
53
+ log.info(`QR code received for user ${userName}, sending to node ${nodeId}`);
54
+ sendEvent("whatsapp.qr", { qr, userId });
55
+ },
56
+ });
57
+ const session = {
58
+ sock,
59
+ connected: false,
60
+ startedAt: Date.now(),
61
+ monitoring: false,
62
+ nodeId,
63
+ userId,
64
+ userName,
65
+ sendNodeEvent: sendEvent,
66
+ };
67
+ activeHostedSessions.set(userId, session);
68
+ // Handle connection updates
69
+ sock.ev.on("connection.update", (update) => {
70
+ if (update.connection === "open") {
71
+ log.info(`WhatsApp connected for user ${userName}`);
72
+ session.connected = true;
73
+ // Read the linked phone number
74
+ const currentSelfId = readWebSelfId(authDir);
75
+ sendEvent("whatsapp.connected", {
76
+ userId,
77
+ phone: currentSelfId.e164 ?? currentSelfId.jid,
78
+ message: "WhatsApp connected successfully!",
79
+ });
80
+ // Start message monitoring if not already
81
+ if (!session.monitoring) {
82
+ session.monitoring = true;
83
+ startMessageMonitoring(session, authDir);
84
+ }
85
+ }
86
+ else if (update.connection === "close") {
87
+ log.info(`WhatsApp connection closed for user ${userName}`);
88
+ session.connected = false;
89
+ session.monitoring = false;
90
+ // Don't delete session - allow reconnection
91
+ }
92
+ });
93
+ // If already authenticated, connection should open automatically
94
+ if (selfId.jid) {
95
+ log.info(`User ${userName} already has WhatsApp linked: ${selfId.e164 ?? selfId.jid}`);
96
+ }
97
+ }
98
+ catch (err) {
99
+ log.error(`Failed to start WhatsApp for user ${userName}`, { error: String(err) });
100
+ sendEvent("whatsapp.error", {
101
+ userId,
102
+ error: `Failed to start WhatsApp: ${String(err)}`,
103
+ });
104
+ }
105
+ }
106
+ /**
107
+ * Start monitoring WhatsApp messages for a hosted user.
108
+ */
109
+ function startMessageMonitoring(session, authDir) {
110
+ const { sock, userId, userName, nodeId, sendNodeEvent } = session;
111
+ if (!sock) {
112
+ return;
113
+ }
114
+ log.info(`Starting message monitoring for user ${userName}`);
115
+ // Get the user's own JID to filter out own messages
116
+ const selfId = readWebSelfId(authDir);
117
+ const ownJid = selfId.jid;
118
+ sock.ev.on("messages.upsert", async (upsert) => {
119
+ if (upsert.type !== "notify") {
120
+ return;
121
+ }
122
+ for (const msg of upsert.messages) {
123
+ try {
124
+ await handleIncomingMessage(session, msg, ownJid);
125
+ }
126
+ catch (err) {
127
+ log.error(`Error handling message for user ${userName}`, { error: String(err) });
128
+ }
129
+ }
130
+ });
131
+ log.info(`Message monitoring started for user ${userName}`);
132
+ }
133
+ /**
134
+ * Handle an incoming WhatsApp message for a hosted user.
135
+ */
136
+ async function handleIncomingMessage(session, msg, ownJid) {
137
+ const { sock, userId, userName, nodeId, sendNodeEvent } = session;
138
+ if (!sock) {
139
+ return;
140
+ }
141
+ // Skip if no message content or key
142
+ if (!msg.message || !msg.key) {
143
+ return;
144
+ }
145
+ // Skip own messages (echo)
146
+ const fromMe = msg.key.fromMe;
147
+ if (fromMe) {
148
+ return;
149
+ }
150
+ // Extract message text
151
+ const text = extractText(msg.message);
152
+ if (!text || !text.trim()) {
153
+ return;
154
+ }
155
+ // Get sender info
156
+ const remoteJid = msg.key.remoteJid;
157
+ if (!remoteJid) {
158
+ return;
159
+ }
160
+ // Determine if it's a group or DM
161
+ const isGroup = remoteJid.endsWith("@g.us");
162
+ const senderJid = isGroup ? (msg.key.participant ?? remoteJid) : remoteJid;
163
+ log.info(`Message from ${senderJid} for user ${userName}: ${text.slice(0, 50)}...`);
164
+ // Notify the node that a message is being processed
165
+ sendNodeEvent("whatsapp.message.received", {
166
+ userId,
167
+ from: senderJid,
168
+ text: text.slice(0, 100),
169
+ isGroup,
170
+ });
171
+ try {
172
+ // Send typing indicator
173
+ await sock.presenceSubscribe(remoteJid);
174
+ await sock.sendPresenceUpdate("composing", remoteJid);
175
+ // Process through AI agent
176
+ const reply = await runHostedAgentReply({
177
+ userId,
178
+ userName,
179
+ nodeId,
180
+ messageText: text,
181
+ senderJid: senderJid ?? remoteJid,
182
+ chatJid: remoteJid,
183
+ isGroup,
184
+ sendNodeEvent,
185
+ });
186
+ // Stop typing indicator
187
+ await sock.sendPresenceUpdate("paused", remoteJid);
188
+ if (reply && reply.trim()) {
189
+ // Send reply via WhatsApp
190
+ const authDir = resolveHostedWhatsAppAuthDir(userId);
191
+ await sendMessageWhatsApp(remoteJid, reply, {
192
+ verbose: false,
193
+ accountId: userId, // Use userId as account for hosted mode
194
+ });
195
+ log.info(`Reply sent to ${remoteJid} for user ${userName}`);
196
+ sendNodeEvent("whatsapp.message.sent", {
197
+ userId,
198
+ to: remoteJid,
199
+ text: reply.slice(0, 100),
200
+ });
201
+ }
202
+ }
203
+ catch (err) {
204
+ log.error(`Failed to process/reply message for user ${userName}`, { error: String(err) });
205
+ sendNodeEvent("whatsapp.message.error", {
206
+ userId,
207
+ error: String(err),
208
+ });
209
+ }
210
+ }
211
+ /**
212
+ * Check if a hosted user has WhatsApp connected.
213
+ */
214
+ export async function isHostedUserWhatsAppConnected(userId) {
215
+ const session = activeHostedSessions.get(userId);
216
+ if (session?.connected) {
217
+ return true;
218
+ }
219
+ const authDir = resolveHostedWhatsAppAuthDir(userId);
220
+ const hasAuth = await webAuthExists(authDir);
221
+ if (!hasAuth) {
222
+ return false;
223
+ }
224
+ const selfId = readWebSelfId(authDir);
225
+ return Boolean(selfId.jid);
226
+ }
227
+ /**
228
+ * Get an active session for a hosted user.
229
+ */
230
+ export function getHostedSession(userId) {
231
+ return activeHostedSessions.get(userId);
232
+ }
233
+ /**
234
+ * Clean up WhatsApp session for a hosted user.
235
+ */
236
+ export function cleanupHostedWhatsAppSession(userId) {
237
+ const session = activeHostedSessions.get(userId);
238
+ if (session?.sock) {
239
+ try {
240
+ session.sock.end(new Error("cleanup"));
241
+ }
242
+ catch {
243
+ // Ignore cleanup errors
244
+ }
245
+ }
246
+ activeHostedSessions.delete(userId);
247
+ }
@@ -10,6 +10,9 @@ import { rawDataToString } from "../../../infra/ws.js";
10
10
  import { isGatewayCliClient, isWebchatClient } from "../../../utils/message-channel.js";
11
11
  import { authorizeGatewayConnect, isLocalDirectRequest } from "../../auth.js";
12
12
  import { buildDeviceAuthPayload } from "../../device-auth.js";
13
+ import { registerHostedUserNode } from "../../hosted-db.js";
14
+ import { startHostedTelegramBot } from "../../hosted-telegram.js";
15
+ import { startHostedWhatsAppPairing } from "../../hosted-whatsapp.js";
13
16
  import { isLoopbackAddress, isTrustedProxyAddress, resolveGatewayClientIp } from "../../net.js";
14
17
  import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
15
18
  import { GATEWAY_CLIENT_IDS } from "../../protocol/client-info.js";
@@ -488,7 +491,8 @@ export function attachGatewayWsMessageHandler(params) {
488
491
  close(1008, truncateCloseReason(authMessage));
489
492
  return;
490
493
  }
491
- const skipPairing = allowControlUiBypass && hasSharedAuth;
494
+ // Skip pairing for: (1) control UI with shared auth, or (2) hosted-token auth
495
+ const skipPairing = (allowControlUiBypass && hasSharedAuth) || authMethod === "hosted-token";
492
496
  if (device && devicePublicKey && !skipPairing) {
493
497
  const requirePairing = async (reason, _paired) => {
494
498
  const pairing = await requestDevicePairing({
@@ -719,6 +723,33 @@ export function attachGatewayWsMessageHandler(params) {
719
723
  });
720
724
  })
721
725
  .catch((err) => logGateway.warn(`voicewake snapshot failed for ${nodeSession.nodeId}: ${formatForLog(err)}`));
726
+ // For hosted-token authenticated nodes, start messaging channels
727
+ if (authMethod === "hosted-token" && authResult.hostedUser) {
728
+ const { userId, userName, telegramBotToken } = authResult.hostedUser;
729
+ registerHostedUserNode(userId, nodeSession.nodeId);
730
+ const sendEvent = (event, payload) => {
731
+ context.nodeRegistry.sendEvent(nodeSession.nodeId, event, payload);
732
+ };
733
+ // Start Telegram bot if configured
734
+ if (telegramBotToken) {
735
+ void startHostedTelegramBot({
736
+ userId,
737
+ userName,
738
+ nodeId: nodeSession.nodeId,
739
+ botToken: telegramBotToken,
740
+ sendEvent,
741
+ }).catch((err) => logGateway.warn(`hosted Telegram bot failed for ${userName}: ${formatForLog(err)}`));
742
+ }
743
+ else {
744
+ // Fall back to WhatsApp pairing if no Telegram
745
+ void startHostedWhatsAppPairing({
746
+ userId,
747
+ userName,
748
+ nodeId: nodeSession.nodeId,
749
+ sendEvent,
750
+ }).catch((err) => logGateway.warn(`hosted WhatsApp pairing failed for ${userName}: ${formatForLog(err)}`));
751
+ }
752
+ }
722
753
  }
723
754
  logWs("out", "hello-ok", {
724
755
  connId,
@@ -21,8 +21,8 @@ import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/di
21
21
  import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
22
22
  import { runOnboardingWizard } from "../wizard/onboarding.js";
23
23
  import { startGatewayConfigReloader } from "./config-reload.js";
24
- import { initHostedDb, isHostedMode } from "./hosted-db.js";
25
24
  import { ExecApprovalManager } from "./exec-approval-manager.js";
25
+ import { initHostedDb, isHostedMode } from "./hosted-db.js";
26
26
  import { NodeRegistry } from "./node-registry.js";
27
27
  import { createChannelManager } from "./server-channels.js";
28
28
  import { createAgentEventHandler } from "./server-chat.js";
@@ -758,7 +758,9 @@ async function handleInvoke(frame, client, skillBins) {
758
758
  security === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
759
759
  segments = analysis.segments;
760
760
  }
761
- const useMacAppExec = process.platform === "darwin";
761
+ // Skip macOS app exec host in hosted mode - use direct spawn instead
762
+ // The CLI is running, not the macOS app, so the app socket may be stale or unavailable
763
+ const useMacAppExec = process.platform === "darwin" && params.hostedMode !== true;
762
764
  if (useMacAppExec) {
763
765
  const approvalDecision = params.approvalDecision === "allow-once" || params.approvalDecision === "allow-always"
764
766
  ? params.approvalDecision
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agiagent-dev",
3
- "version": "2026.1.32",
3
+ "version": "2026.1.33",
4
4
  "description": "AI assistant CLI",
5
5
  "keywords": [],
6
6
  "license": "MIT",