agiagent-dev 2026.1.35 → 2026.1.37

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.
@@ -551,10 +551,15 @@ export function createExecTool(defaults) {
551
551
  const parsedAgentSession = parseAgentSessionKey(defaults?.sessionKey);
552
552
  const agentId = defaults?.agentId ??
553
553
  (parsedAgentSession ? resolveAgentIdFromSessionKey(defaults?.sessionKey) : undefined);
554
+ // In hosted mode, update description to clarify commands run on user's device
555
+ const isHostedMode = process.env.AGIAGENT_HOSTED_MODE === "1";
556
+ const execDescription = isHostedMode
557
+ ? "Execute shell commands on the user's connected device. Commands run on their Mac/PC, not on this server. Use yieldMs/background for long-running commands. Use pty=true for TTY-required commands."
558
+ : "Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. Use pty=true for TTY-required commands (terminal UIs, coding agents).";
554
559
  return {
555
560
  name: "exec",
556
561
  label: "exec",
557
- description: "Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. Use pty=true for TTY-required commands (terminal UIs, coding agents).",
562
+ description: execDescription,
558
563
  parameters: execSchema,
559
564
  execute: async (_toolCallId, args, signal, onUpdate) => {
560
565
  const params = args;
@@ -167,10 +167,8 @@ export function createBrowserTool(opts) {
167
167
  name: "browser",
168
168
  description: [
169
169
  "Control the browser via AGIAgent's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions/list_upload_inputs/attach_file).",
170
- 'Profiles: use profile="chrome" for Chrome extension relay takeover (your existing Chrome tabs). Use profile="agiagent" for the isolated agiagent-managed browser.',
171
- 'If the user mentions the Chrome extension / Browser Relay / toolbar button / “attach tab”, ALWAYS use profile="chrome" (do not ask which profile).',
170
+ 'ALWAYS use profile="agiagent". This is the isolated agiagent-managed browser. Do NOT use any other profile. Never use profile="chrome".',
172
171
  'When a node-hosted browser proxy is available, the tool may auto-route to it. Pin a node with node=<id|name> or target="node".',
173
- "Chrome extension relay needs an attached tab: user must click the AGIAgent Browser Relay toolbar icon on the tab (badge ON). If no tab is connected, ask them to attach it.",
174
172
  "When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc).",
175
173
  'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.',
176
174
  "Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
@@ -223,7 +223,7 @@ export async function launchAGIAgentChrome(resolved, profile) {
223
223
  throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}".`);
224
224
  }
225
225
  const pid = proc.pid ?? -1;
226
- log.info(`🦞 agiagent browser started (${exe.kind}) profile "${profile.name}" on 127.0.0.1:${profile.cdpPort} (pid ${pid})`);
226
+ log.debug(`agiagent browser started (${exe.kind}) profile "${profile.name}" on 127.0.0.1:${profile.cdpPort} (pid ${pid})`);
227
227
  return {
228
228
  pid,
229
229
  exe,
@@ -37,10 +37,10 @@ export async function startBrowserControlServiceFromConfig() {
37
37
  continue;
38
38
  }
39
39
  await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
40
- logService.warn(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
40
+ logService.debug(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
41
41
  });
42
42
  }
43
- logService.info(`Browser control service ready (profiles=${Object.keys(resolved.profiles).length})`);
43
+ logService.debug(`Browser control service ready (profiles=${Object.keys(resolved.profiles).length})`);
44
44
  return state;
45
45
  }
46
46
  export async function stopBrowserControlService() {
@@ -48,10 +48,10 @@ export async function startBrowserControlServerFromConfig() {
48
48
  continue;
49
49
  }
50
50
  await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
51
- logServer.warn(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
51
+ logServer.debug(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
52
52
  });
53
53
  }
54
- logServer.info(`Browser control listening on http://127.0.0.1:${port}/`);
54
+ logServer.debug(`Browser control listening on http://127.0.0.1:${port}/`);
55
55
  return state;
56
56
  }
57
57
  export async function stopBrowserControlServer() {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.1.35",
3
- "commit": "60d46f62b6417f469753f0bc785f4481b74c76eb",
4
- "builtAt": "2026-02-03T06:50:10.937Z"
2
+ "version": "2026.1.37",
3
+ "commit": "13a7d3ba17f463a5e944081450d1ae77fa6bccfc",
4
+ "builtAt": "2026-02-04T07:17:31.327Z"
5
5
  }
@@ -1 +1 @@
1
- c2e45112d75a09927b99786eaa6500c5433a73d88440d47ec00a0598c2359d7f
1
+ 38a7a2ab6bf18c38ec8f8da6d1e4e7d01023ef6af2bc42a519eba3f5ad285219
@@ -1,7 +1,6 @@
1
1
  import { type TaglineOptions } from "./tagline.js";
2
2
  type BannerOptions = TaglineOptions & {
3
3
  argv?: string[];
4
- commit?: string | null;
5
4
  columns?: number;
6
5
  richTty?: boolean;
7
6
  };
@@ -1,4 +1,3 @@
1
- import { resolveCommitHash } from "../infra/git-commit.js";
2
1
  import { visibleWidth } from "../terminal/ansi.js";
3
2
  import { isRich, theme } from "../terminal/theme.js";
4
3
  import { pickTagline } from "./tagline.js";
@@ -20,27 +19,25 @@ function splitGraphemes(value) {
20
19
  const hasJsonFlag = (argv) => argv.some((arg) => arg === "--json" || arg.startsWith("--json="));
21
20
  const hasVersionFlag = (argv) => argv.some((arg) => arg === "--version" || arg === "-V" || arg === "-v");
22
21
  export function formatCliBannerLine(version, options = {}) {
23
- const commit = options.commit ?? resolveCommitHash({ env: options.env });
24
- const commitLabel = commit ?? "unknown";
25
22
  const tagline = pickTagline(options);
26
23
  const rich = options.richTty ?? isRich();
27
24
  const title = "🦞 AGIAgent";
28
25
  const prefix = "🦞 ";
29
26
  const columns = options.columns ?? process.stdout.columns ?? 120;
30
- const plainFullLine = `${title} ${version} (${commitLabel}) — ${tagline}`;
27
+ const plainFullLine = `${title} ${version} — ${tagline}`;
31
28
  const fitsOnOneLine = visibleWidth(plainFullLine) <= columns;
32
29
  if (rich) {
33
30
  if (fitsOnOneLine) {
34
- return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(`(${commitLabel})`)} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
31
+ return `${theme.heading(title)} ${theme.info(version)} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
35
32
  }
36
- const line1 = `${theme.heading(title)} ${theme.info(version)} ${theme.muted(`(${commitLabel})`)}`;
33
+ const line1 = `${theme.heading(title)} ${theme.info(version)}`;
37
34
  const line2 = `${" ".repeat(prefix.length)}${theme.accentDim(tagline)}`;
38
35
  return `${line1}\n${line2}`;
39
36
  }
40
37
  if (fitsOnOneLine) {
41
38
  return plainFullLine;
42
39
  }
43
- const line1 = `${title} ${version} (${commitLabel})`;
40
+ const line1 = `${title} ${version}`;
44
41
  const line2 = `${" ".repeat(prefix.length)}${tagline}`;
45
42
  return `${line1}\n${line2}`;
46
43
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Animated UX helpers for `agiagent connect`.
3
+ * Renders signal art, gradient title, spinners, status box, and pulsing dot.
4
+ */
5
+ export declare function showConnectArt(): Promise<void>;
6
+ export declare function showGreeting(text?: string): Promise<void>;
7
+ export type ConnectSpinner = {
8
+ resolve: (text: string) => void;
9
+ fail: (text: string) => void;
10
+ };
11
+ export declare function createConnectSpinner(text: string): ConnectSpinner;
12
+ export declare function showStatusBox(machineName?: string): void;
13
+ export type ListeningIndicator = {
14
+ stop: () => void;
15
+ };
16
+ export declare function startListeningIndicator(): ListeningIndicator;
17
+ export declare function showReconnecting(msg?: string): void;
18
+ export declare function showReconnected(msg?: string): void;
19
+ export declare function hideCursor(): void;
20
+ export declare function showCursor(): void;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Animated UX helpers for `agiagent connect`.
3
+ * Renders signal art, gradient title, spinners, status box, and pulsing dot.
4
+ */
5
+ import chalk from "chalk";
6
+ import os from "node:os";
7
+ import { LOBSTER_PALETTE } from "../../terminal/palette.js";
8
+ import { isRich } from "../../terminal/theme.js";
9
+ import { VERSION } from "../../version.js";
10
+ const P = LOBSTER_PALETTE;
11
+ // Extended palette tokens used only by connect UX
12
+ const GLOW = "#FFDDC9";
13
+ const WHITE = "#F5F0EB";
14
+ const t = {
15
+ accent: chalk.hex(P.accent),
16
+ bright: chalk.hex(P.accentBright),
17
+ dim: chalk.hex(P.accentDim),
18
+ info: chalk.hex(P.info),
19
+ glow: chalk.hex(GLOW),
20
+ success: chalk.hex(P.success),
21
+ warn: chalk.hex(P.warn),
22
+ muted: chalk.hex(P.muted),
23
+ white: chalk.hex(WHITE),
24
+ bold: chalk.bold.hex(P.accentBright),
25
+ boldWhite: chalk.bold.hex(WHITE),
26
+ };
27
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
28
+ function stripAnsi(s) {
29
+ // eslint-disable-next-line no-control-regex
30
+ return s.replace(/\u001B\[[0-9;]*m/g, "");
31
+ }
32
+ // ── Signal art ──────────────────────────────────────────────
33
+ const ART_SIGNAL = [
34
+ " . ",
35
+ " ·· | ·· ",
36
+ " ·· | ·· ",
37
+ " · ◉ · ",
38
+ " ·· | ·· ",
39
+ " ·· | ·· ",
40
+ " · ",
41
+ ];
42
+ const ART_GRADIENT = [
43
+ P.muted,
44
+ P.accentDim,
45
+ P.accent,
46
+ P.accentBright,
47
+ P.accent,
48
+ P.accentDim,
49
+ P.muted,
50
+ ];
51
+ export async function showConnectArt() {
52
+ if (!isRich()) {
53
+ return;
54
+ }
55
+ process.stdout.write("\n\n");
56
+ for (let i = 0; i < ART_SIGNAL.length; i++) {
57
+ const ci = Math.floor((i / ART_SIGNAL.length) * ART_GRADIENT.length);
58
+ process.stdout.write(`${chalk.hex(ART_GRADIENT[ci])(` ${ART_SIGNAL[i]}`)}\n`);
59
+ await sleep(50);
60
+ }
61
+ process.stdout.write("\n");
62
+ // Gradient bold title
63
+ const titleText = " A G I A G E N T";
64
+ const colors = [P.accentDim, P.accent, P.accentBright, P.info, GLOW];
65
+ const gradientTitle = Array.from(titleText)
66
+ .map((ch, i) => {
67
+ const ci2 = Math.floor((i / titleText.length) * colors.length);
68
+ return chalk.bold.hex(colors[Math.min(ci2, colors.length - 1)])(ch);
69
+ })
70
+ .join("");
71
+ process.stdout.write(` ${gradientTitle}\n`);
72
+ process.stdout.write(` ${t.muted(` v${VERSION}`)}\n`);
73
+ process.stdout.write("\n");
74
+ await sleep(400);
75
+ }
76
+ // ── Typewriter greeting ─────────────────────────────────────
77
+ export async function showGreeting(text = "Hey — just a moment while I get everything ready.") {
78
+ if (!isRich()) {
79
+ process.stdout.write(` ${text}\n\n`);
80
+ return;
81
+ }
82
+ process.stdout.write(" ");
83
+ for (const ch of text) {
84
+ process.stdout.write(t.white(ch));
85
+ await sleep(28);
86
+ }
87
+ process.stdout.write("\n\n");
88
+ await sleep(500);
89
+ }
90
+ // ── Spinner ─────────────────────────────────────────────────
91
+ const DOTS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
92
+ let dotIndex = 0;
93
+ const nextDot = () => t.accent(DOTS[dotIndex++ % DOTS.length]);
94
+ export function createConnectSpinner(text) {
95
+ let running = true;
96
+ let timer;
97
+ if (isRich()) {
98
+ timer = setInterval(() => {
99
+ if (!running) {
100
+ return;
101
+ }
102
+ process.stdout.write(`\r ${nextDot()} ${t.white(text)}\x1b[K`);
103
+ }, 80);
104
+ }
105
+ else {
106
+ process.stdout.write(` ... ${text}\n`);
107
+ }
108
+ return {
109
+ resolve(doneText) {
110
+ running = false;
111
+ if (timer) {
112
+ clearInterval(timer);
113
+ }
114
+ if (isRich()) {
115
+ process.stdout.write(`\r ${t.success("✓")} ${t.white(doneText)}\x1b[K\n`);
116
+ }
117
+ else {
118
+ process.stdout.write(` ✓ ${doneText}\n`);
119
+ }
120
+ },
121
+ fail(failText) {
122
+ running = false;
123
+ if (timer) {
124
+ clearInterval(timer);
125
+ }
126
+ if (isRich()) {
127
+ process.stdout.write(`\r ${t.warn("✗")} ${t.info(failText)}\x1b[K\n`);
128
+ }
129
+ else {
130
+ process.stdout.write(` ✗ ${failText}\n`);
131
+ }
132
+ },
133
+ };
134
+ }
135
+ // ── Status box ──────────────────────────────────────────────
136
+ export function showStatusBox(machineName) {
137
+ const name = machineName ?? os.hostname().replace(/\.local$/, "");
138
+ const width = 50;
139
+ const lines = [
140
+ "",
141
+ `${t.bold("I'm ready.")}`,
142
+ "",
143
+ `${t.white(`Running on ${name}`)}`,
144
+ `${t.muted("Status:")} ${t.success("● Online")}`,
145
+ "",
146
+ `${t.white("Send me a message from WhatsApp, Telegram,")}`,
147
+ `${t.white("or any channel you've connected — I'll take")}`,
148
+ `${t.white("it from here.")}`,
149
+ "",
150
+ ];
151
+ if (!isRich()) {
152
+ process.stdout.write(`\n --- I'm ready. Running on ${name} (Online) ---\n\n`);
153
+ return;
154
+ }
155
+ const top = ` ${t.dim("╭")}${t.dim("─".repeat(width))}${t.dim("╮")}`;
156
+ const bot = ` ${t.dim("╰")}${t.dim("─".repeat(width))}${t.dim("╯")}`;
157
+ const rows = lines.map((l) => {
158
+ const vis = stripAnsi(l).length;
159
+ const pad = Math.max(0, width - 2 - vis);
160
+ return ` ${t.dim("│")} ${l}${" ".repeat(pad)} ${t.dim("│")}`;
161
+ });
162
+ process.stdout.write(`\n${[top, ...rows, bot].join("\n")}\n`);
163
+ process.stdout.write(`\n ${t.muted("Ctrl+C whenever you want to stop.")}\n\n`);
164
+ }
165
+ const PULSE_SHADES = ["#2FBF71", "#3DD685", "#5AE89E", "#3DD685", "#2FBF71", "#228B57"];
166
+ export function startListeningIndicator() {
167
+ let running = true;
168
+ let pulseI = 0;
169
+ let timer;
170
+ if (isRich()) {
171
+ timer = setInterval(() => {
172
+ if (!running) {
173
+ return;
174
+ }
175
+ const shade = PULSE_SHADES[pulseI++ % PULSE_SHADES.length];
176
+ process.stdout.write(`\r ${chalk.hex(shade)("●")} ${t.muted("Listening...")}\x1b[K`);
177
+ }, 200);
178
+ }
179
+ else {
180
+ process.stdout.write(" ● Listening...\n");
181
+ }
182
+ return {
183
+ stop() {
184
+ running = false;
185
+ if (timer) {
186
+ clearInterval(timer);
187
+ }
188
+ if (isRich()) {
189
+ process.stdout.write("\r\x1b[K");
190
+ }
191
+ },
192
+ };
193
+ }
194
+ // ── Reconnect messages ──────────────────────────────────────
195
+ export function showReconnecting(msg = "Hmm, lost the connection — hold on...") {
196
+ if (isRich()) {
197
+ process.stdout.write(`\r ${t.warn("↻")} ${t.info(msg)}\x1b[K\n`);
198
+ }
199
+ else {
200
+ process.stdout.write(` ↻ ${msg}\n`);
201
+ }
202
+ }
203
+ export function showReconnected(msg = "And we're back. No worries.") {
204
+ if (isRich()) {
205
+ process.stdout.write(`\r ${t.success("✓")} ${t.white(msg)}\x1b[K\n`);
206
+ }
207
+ else {
208
+ process.stdout.write(` ✓ ${msg}\n`);
209
+ }
210
+ }
211
+ // ── Cursor helpers ──────────────────────────────────────────
212
+ export function hideCursor() {
213
+ if (isRich()) {
214
+ process.stdout.write("\x1b[?25l");
215
+ }
216
+ }
217
+ export function showCursor() {
218
+ if (isRich()) {
219
+ process.stdout.write("\x1b[?25h");
220
+ }
221
+ }
@@ -9,6 +9,7 @@
9
9
  import { saveNodeHostConfig, ensureNodeHostConfig } from "../../node-host/config.js";
10
10
  import { runNodeHost } from "../../node-host/runner.js";
11
11
  import { theme } from "../../terminal/theme.js";
12
+ import { showConnectArt, showGreeting, createConnectSpinner, showStatusBox, startListeningIndicator, showReconnecting, showReconnected, hideCursor, showCursor, } from "./connect-ui.js";
12
13
  // Default hosted gateway URL (can be overridden via --gateway)
13
14
  const DEFAULT_HOSTED_GATEWAY_HOST = "agiagent-hosted.fly.dev";
14
15
  const DEFAULT_HOSTED_GATEWAY_PORT = 443;
@@ -77,12 +78,9 @@ ${theme.muted("Your WhatsApp messages will trigger AI that runs commands on this
77
78
  existing.displayName = opts.displayName;
78
79
  }
79
80
  await saveNodeHostConfig(existing);
80
- console.log(`\n${theme.success("Connecting to hosted AGIAgent...")}`);
81
- console.log(` Gateway: ${useTls ? "wss" : "ws"}://${host}:${port}`);
82
- console.log(` Token: ${token.slice(0, 8)}...`);
83
- console.log("");
84
81
  if (opts.install) {
85
82
  // Install as background service
83
+ console.log(`\n${theme.success("Connecting to hosted AGIAgent...")}`);
86
84
  console.log(`${theme.info("Installing as background service...")}`);
87
85
  console.log(`${theme.muted("(Use 'agiagent node uninstall' to remove)")}\n`);
88
86
  // Import and run the daemon install
@@ -97,8 +95,23 @@ ${theme.muted("Your WhatsApp messages will trigger AI that runs commands on this
97
95
  });
98
96
  }
99
97
  else {
100
- // Run in foreground
101
- console.log(`${theme.muted("Press Ctrl+C to disconnect.")}\n`);
98
+ // Run in foreground with animated UX
99
+ hideCursor();
100
+ const cleanup = () => showCursor();
101
+ process.on("exit", cleanup);
102
+ process.on("SIGINT", () => {
103
+ cleanup();
104
+ process.exit(0);
105
+ });
106
+ // Signal art + gradient title + greeting
107
+ await showConnectArt();
108
+ await showGreeting();
109
+ // Start "Getting online..." spinner — resolved by onConnected callback
110
+ const onlineSpinner = createConnectSpinner("Getting online...");
111
+ let browserSpinner = null;
112
+ let channelsSpinner = null;
113
+ let listening = null;
114
+ let connected = false;
102
115
  // Set the token as an environment variable for the node host to use
103
116
  process.env.AGIAGENT_GATEWAY_TOKEN = token;
104
117
  await runNodeHost({
@@ -106,6 +119,43 @@ ${theme.muted("Your WhatsApp messages will trigger AI that runs commands on this
106
119
  gatewayPort: port,
107
120
  gatewayTls: useTls,
108
121
  displayName: opts.displayName,
122
+ onConnected: () => {
123
+ if (!connected) {
124
+ connected = true;
125
+ onlineSpinner.resolve("We're connected");
126
+ // Browser spinner
127
+ browserSpinner = createConnectSpinner("Opening up a browser for you...");
128
+ setTimeout(() => {
129
+ browserSpinner?.resolve("Browser is good to go");
130
+ // Channels spinner
131
+ channelsSpinner = createConnectSpinner("Checking your message channels...");
132
+ setTimeout(() => {
133
+ channelsSpinner?.resolve("All channels are active");
134
+ showStatusBox(opts.displayName);
135
+ listening = startListeningIndicator();
136
+ }, 1000);
137
+ }, 1800);
138
+ }
139
+ else {
140
+ // Reconnected after a drop
141
+ showReconnected();
142
+ listening = startListeningIndicator();
143
+ }
144
+ },
145
+ onConnectError: () => {
146
+ if (connected) {
147
+ listening?.stop();
148
+ listening = null;
149
+ showReconnecting();
150
+ }
151
+ },
152
+ onClose: () => {
153
+ if (connected) {
154
+ listening?.stop();
155
+ listening = null;
156
+ showReconnecting();
157
+ }
158
+ },
109
159
  });
110
160
  }
111
161
  });
@@ -11,10 +11,46 @@ import { getReplyFromConfig } from "../auto-reply/reply.js";
11
11
  import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
12
12
  import { loadConfig } from "../config/config.js";
13
13
  import { resolveStateDir } from "../config/paths.js";
14
+ import { getPairedNode } from "../infra/node-pairing.js";
14
15
  import { createSubsystemLogger } from "../logging/subsystem.js";
15
16
  import { normalizeAgentId } from "../routing/session-key.js";
16
17
  import { jidToE164 } from "../utils.js";
17
18
  const log = createSubsystemLogger("hosted-agent");
19
+ /**
20
+ * Build the hosted mode system prompt context.
21
+ * This tells the AI that shell commands run on the user's connected device.
22
+ */
23
+ function buildHostedModeSystemPrompt(params) {
24
+ const nodeLabel = params.nodeName || params.nodeId.slice(0, 12);
25
+ return `## Hosted Mode
26
+
27
+ You are running in **hosted mode**. Important:
28
+
29
+ 1. **Shell commands run on the user's connected device** ("${nodeLabel}"), not on this server.
30
+ 2. Use the \`exec\` tool to run shell commands - they will execute on the user's Mac/PC.
31
+ 3. Use the \`browser\` tool to control the browser on the user's device.
32
+ 4. The user's device is connected and ready to receive commands.
33
+
34
+ Do NOT say "shell isn't available" or "I can't run commands" - you CAN run commands via the exec tool on the connected device.`;
35
+ }
36
+ /**
37
+ * Get node info for the connected node.
38
+ */
39
+ async function getConnectedNodeInfo(nodeId) {
40
+ try {
41
+ const node = await getPairedNode(nodeId);
42
+ if (!node) {
43
+ return { name: nodeId.slice(0, 12), paired: false };
44
+ }
45
+ return {
46
+ name: node.displayName || nodeId.slice(0, 12),
47
+ paired: true,
48
+ };
49
+ }
50
+ catch {
51
+ return { name: nodeId.slice(0, 12), paired: false };
52
+ }
53
+ }
18
54
  const HOSTED_DIR_SEGMENT = "hosted";
19
55
  const HOSTED_USERS_DIR_SEGMENT = "users";
20
56
  const HOSTED_AGENT_PREFIX = "hosted";
@@ -75,8 +111,17 @@ export async function runHostedAgentReply(params) {
75
111
  const hostedScope = resolveHostedAgentScope({ userId, chatJid });
76
112
  const userSessionKey = hostedScope.sessionKey;
77
113
  const senderE164 = jidToE164(senderJid) ?? senderJid;
78
- log.info(`Routing to session ${userSessionKey} for user ${userName}`);
114
+ // Get connected node info for system prompt context
115
+ const nodeInfo = await getConnectedNodeInfo(nodeId);
116
+ const nodeName = nodeInfo.name || `node-${nodeId.slice(0, 8)}`;
117
+ // Build hosted mode system prompt to tell the AI about the connected node
118
+ const hostedSystemPrompt = buildHostedModeSystemPrompt({
119
+ nodeId,
120
+ nodeName,
121
+ });
122
+ log.info(`Routing to session ${userSessionKey} for user ${userName} (node: ${nodeName})`);
79
123
  // Build the message context using the existing infrastructure
124
+ // Include the hosted mode system prompt via GroupSystemPrompt to inject it into the AI context
80
125
  const ctx = finalizeInboundContext({
81
126
  Body: messageText,
82
127
  RawBody: messageText,
@@ -99,6 +144,8 @@ export async function runHostedAgentReply(params) {
99
144
  Surface: "telegram",
100
145
  OriginatingChannel: "telegram",
101
146
  OriginatingTo: chatJid,
147
+ // Inject hosted mode context into the system prompt
148
+ GroupSystemPrompt: hostedSystemPrompt,
102
149
  });
103
150
  const defaultAgentId = resolveDefaultAgentId(cfg);
104
151
  const sourceAgentDir = resolveAgentDir(cfg, defaultAgentId);
@@ -8,10 +8,33 @@
8
8
  * 4. Sends responses
9
9
  */
10
10
  import { Bot } from "grammy";
11
+ import { getPairedNode } from "../infra/node-pairing.js";
11
12
  import { createSubsystemLogger } from "../logging/subsystem.js";
12
13
  import { runHostedAgentReply } from "./hosted-agent.js";
13
14
  import { getHostedDb, isHostedMode } from "./hosted-db.js";
14
15
  const log = createSubsystemLogger("hosted-telegram");
16
+ /**
17
+ * Wait for a node to be paired before processing messages.
18
+ * This prevents the AI from saying "not paired" when the device is connecting.
19
+ */
20
+ async function waitForNodePaired(nodeId, maxWaitMs = 3000, pollIntervalMs = 200) {
21
+ const startTime = Date.now();
22
+ while (Date.now() - startTime < maxWaitMs) {
23
+ try {
24
+ const node = await getPairedNode(nodeId);
25
+ if (node) {
26
+ return true;
27
+ }
28
+ }
29
+ catch {
30
+ // Ignore errors during polling
31
+ }
32
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
33
+ }
34
+ // Return true anyway - we'll let the AI handle it if not paired
35
+ // This prevents blocking forever if there's an issue
36
+ return true;
37
+ }
15
38
  const activeHostedBots = new Map();
16
39
  /**
17
40
  * Start a Telegram bot for a hosted user.
@@ -97,6 +120,9 @@ export async function startHostedTelegramBot(params) {
97
120
  await ctx.reply("This bot is private.");
98
121
  return;
99
122
  }
123
+ // Wait for the node to be paired before processing
124
+ // This prevents the AI from saying "not paired" during connection setup
125
+ await waitForNodePaired(session.nodeId, 3000, 200);
100
126
  // Notify node
101
127
  session.sendNodeEvent("telegram.message.received", {
102
128
  userId,
@@ -1,4 +1,4 @@
1
- export const MAX_PAYLOAD_BYTES = 512 * 1024; // cap incoming frame size
1
+ export const MAX_PAYLOAD_BYTES = 25 * 1024 * 1024; // cap incoming frame size (25 MB to match client limit for browser screenshots)
2
2
  export const MAX_BUFFERED_BYTES = 1.5 * 1024 * 1024; // per-connection send buffer limit
3
3
  const DEFAULT_MAX_CHAT_HISTORY_MESSAGES_BYTES = 6 * 1024 * 1024; // keep history responses comfortably under client WS limits
4
4
  let maxChatHistoryMessagesBytes = DEFAULT_MAX_CHAT_HISTORY_MESSAGES_BYTES;
@@ -3,7 +3,12 @@ export function buildNodeShellCommand(command, platform) {
3
3
  .trim()
4
4
  .toLowerCase();
5
5
  if (normalized.startsWith("win")) {
6
- return ["cmd.exe", "/d", "/s", "/c", command];
6
+ // Use PowerShell instead of cmd.exe on Windows.
7
+ // Problem: Many Windows system utilities (ipconfig, systeminfo, dir, etc.) write
8
+ // directly to the console via WriteConsole API, bypassing stdout pipes.
9
+ // When Node.js spawns cmd.exe with piped stdio, these utilities produce no output.
10
+ // PowerShell properly captures and redirects their output to stdout.
11
+ return ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", command];
7
12
  }
8
13
  return ["/bin/sh", "-lc", command];
9
14
  }
@@ -5,6 +5,9 @@ type NodeHostRunOptions = {
5
5
  gatewayTlsFingerprint?: string;
6
6
  nodeId?: string;
7
7
  displayName?: string;
8
+ onConnected?: () => void;
9
+ onConnectError?: (err: Error) => void;
10
+ onClose?: (code: number, reason: string) => void;
8
11
  };
9
12
  type NodeInvokeRequestPayload = {
10
13
  id: string;
@@ -413,8 +413,6 @@ export async function runNodeHost(opts) {
413
413
  const scheme = gateway.tls ? "wss" : "ws";
414
414
  const url = `${scheme}://${host}:${port}`;
415
415
  const pathEnv = ensureNodePathEnv();
416
- // eslint-disable-next-line no-console
417
- console.log(`node host PATH: ${pathEnv}`);
418
416
  const client = new GatewayClient({
419
417
  url,
420
418
  token: token?.trim() || undefined,
@@ -466,14 +464,18 @@ export async function runNodeHost(opts) {
466
464
  return;
467
465
  }
468
466
  },
467
+ onHelloOk: () => {
468
+ opts.onConnected?.();
469
+ },
469
470
  onConnectError: (err) => {
470
- // keep retrying (handled by GatewayClient)
471
- // eslint-disable-next-line no-console
472
- console.error(`node host gateway connect failed: ${err.message}`);
471
+ if (opts.onConnectError) {
472
+ opts.onConnectError(err);
473
+ }
473
474
  },
474
475
  onClose: (code, reason) => {
475
- // eslint-disable-next-line no-console
476
- console.error(`node host gateway closed (${code}): ${reason}`);
476
+ if (opts.onClose) {
477
+ opts.onClose(code, reason);
478
+ }
477
479
  },
478
480
  });
479
481
  const skillBins = new SkillBinsCache(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agiagent-dev",
3
- "version": "2026.1.35",
3
+ "version": "2026.1.37",
4
4
  "description": "AI assistant CLI",
5
5
  "keywords": [],
6
6
  "license": "MIT",