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.
- package/dist/agents/bash-tools.exec.js +6 -1
- package/dist/agents/tools/browser-tool.js +1 -3
- package/dist/browser/chrome.js +1 -1
- package/dist/browser/control-service.js +2 -2
- package/dist/browser/server.js +2 -2
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/cli/banner.d.ts +0 -1
- package/dist/cli/banner.js +4 -7
- package/dist/cli/connect-cli/connect-ui.d.ts +20 -0
- package/dist/cli/connect-cli/connect-ui.js +221 -0
- package/dist/cli/connect-cli/register.js +56 -6
- package/dist/gateway/hosted-agent.js +48 -1
- package/dist/gateway/hosted-telegram.js +26 -0
- package/dist/gateway/server-constants.js +1 -1
- package/dist/infra/node-shell.js +6 -1
- package/dist/node-host/runner.d.ts +3 -0
- package/dist/node-host/runner.js +9 -7
- package/package.json +1 -1
|
@@ -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:
|
|
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
|
-
'
|
|
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.",
|
package/dist/browser/chrome.js
CHANGED
|
@@ -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.
|
|
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.
|
|
40
|
+
logService.debug(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
logService.
|
|
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() {
|
package/dist/browser/server.js
CHANGED
|
@@ -48,10 +48,10 @@ export async function startBrowserControlServerFromConfig() {
|
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
50
50
|
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
|
|
51
|
-
logServer.
|
|
51
|
+
logServer.debug(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
-
logServer.
|
|
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() {
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
38a7a2ab6bf18c38ec8f8da6d1e4e7d01023ef6af2bc42a519eba3f5ad285219
|
package/dist/cli/banner.d.ts
CHANGED
package/dist/cli/banner.js
CHANGED
|
@@ -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}
|
|
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(
|
|
31
|
+
return `${theme.heading(title)} ${theme.info(version)} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
|
|
35
32
|
}
|
|
36
|
-
const line1 = `${theme.heading(title)} ${theme.info(version)}
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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;
|
package/dist/infra/node-shell.js
CHANGED
|
@@ -3,7 +3,12 @@ export function buildNodeShellCommand(command, platform) {
|
|
|
3
3
|
.trim()
|
|
4
4
|
.toLowerCase();
|
|
5
5
|
if (normalized.startsWith("win")) {
|
|
6
|
-
|
|
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;
|
package/dist/node-host/runner.js
CHANGED
|
@@ -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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
471
|
+
if (opts.onConnectError) {
|
|
472
|
+
opts.onConnectError(err);
|
|
473
|
+
}
|
|
473
474
|
},
|
|
474
475
|
onClose: (code, reason) => {
|
|
475
|
-
|
|
476
|
-
|
|
476
|
+
if (opts.onClose) {
|
|
477
|
+
opts.onClose(code, reason);
|
|
478
|
+
}
|
|
477
479
|
},
|
|
478
480
|
});
|
|
479
481
|
const skillBins = new SkillBinsCache(async () => {
|