piclaw 0.0.19 → 0.0.21
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/.output/nitro.json +1 -1
- package/.output/public/assets/defult-D5RLDUrI.js +1 -0
- package/.output/public/assets/{dist-CMBqBOCp.js → dist-BH_oa-kv.js} +1 -1
- package/.output/public/assets/index-7JvURuHy.js +204 -0
- package/.output/public/assets/index-K43slwjJ.css +1 -0
- package/.output/public/index.html +11 -2
- package/.output/server/_...path_.get.mjs +16 -0
- package/.output/server/_chunks/app.mjs +261 -181
- package/.output/server/_chunks/browser.mjs +4 -1
- package/.output/server/_chunks/config.mjs +4 -0
- package/.output/server/_chunks/db.mjs +32 -28
- package/.output/server/_chunks/device-bus.mjs +123 -0
- package/.output/server/_chunks/dummy.mjs +1 -1
- package/.output/server/_chunks/logger.mjs +23 -0
- package/.output/server/_chunks/login.mjs +1 -1
- package/.output/server/_chunks/notes.mjs +1 -3
- package/.output/server/_chunks/renderer-template.mjs +1 -1
- package/.output/server/_chunks/sandbox.mjs +217 -0
- package/.output/server/_chunks/server.mjs +2302 -122
- package/.output/server/_chunks/terminal.mjs +63 -8
- package/.output/server/_chunks/uploads.mjs +60 -0
- package/.output/server/_chunks/virtual.mjs +192 -54
- package/.output/server/_id_.delete.mjs +5 -2
- package/.output/server/_id_.patch.mjs +2 -0
- package/.output/server/_id_2.delete.mjs +8 -0
- package/.output/server/_jid_.delete.mjs +5 -2
- package/.output/server/_jid_.patch.mjs +37 -4
- package/.output/server/_jid_2.delete.mjs +5 -2
- package/.output/server/_libs/@acemir/cssom+[...].mjs +2269 -1137
- package/.output/server/_libs/@google/genai.mjs +337 -273
- package/.output/server/_libs/@mariozechner/pi-agent-core+[...].mjs +381 -2073
- package/.output/server/_libs/@mariozechner/pi-coding-agent+[...].mjs +231 -131
- package/.output/server/_libs/_.mjs +3 -2
- package/.output/server/_libs/_10.mjs +2 -4
- package/.output/server/_libs/_11.mjs +2 -4
- package/.output/server/_libs/_12.mjs +2 -3
- package/.output/server/_libs/_13.mjs +2 -3
- package/.output/server/_libs/_14.mjs +2 -4
- package/.output/server/_libs/_15.mjs +2 -4
- package/.output/server/_libs/_16.mjs +2 -3
- package/.output/server/_libs/_17.mjs +2 -4
- package/.output/server/_libs/_18.mjs +2 -2
- package/.output/server/_libs/_19.mjs +2 -2
- package/.output/server/_libs/_2.mjs +3 -3
- package/.output/server/_libs/_20.mjs +2 -2
- package/.output/server/_libs/_21.mjs +2 -2
- package/.output/server/_libs/_22.mjs +2 -2
- package/.output/server/_libs/_23.mjs +2 -2
- package/.output/server/_libs/_24.mjs +2 -2
- package/.output/server/_libs/_25.mjs +2 -2
- package/.output/server/_libs/_26.mjs +2 -2
- package/.output/server/_libs/_27.mjs +2 -2
- package/.output/server/_libs/_28.mjs +2 -2
- package/.output/server/_libs/_29.mjs +2 -2
- package/.output/server/_libs/_3.mjs +3 -3
- package/.output/server/_libs/_30.mjs +2 -2
- package/.output/server/_libs/_31.mjs +2 -2
- package/.output/server/_libs/_32.mjs +2 -2
- package/.output/server/_libs/_33.mjs +2 -2
- package/.output/server/_libs/_34.mjs +2 -2
- package/.output/server/_libs/_35.mjs +2 -2
- package/.output/server/_libs/_36.mjs +2 -2
- package/.output/server/_libs/_37.mjs +2 -2
- package/.output/server/_libs/_38.mjs +2 -2
- package/.output/server/_libs/_39.mjs +2 -2
- package/.output/server/_libs/_4.mjs +4 -3
- package/.output/server/_libs/_40.mjs +2 -2
- package/.output/server/_libs/_41.mjs +2 -2
- package/.output/server/_libs/_42.mjs +2 -2
- package/.output/server/_libs/_43.mjs +2 -2
- package/.output/server/_libs/_44.mjs +2 -2
- package/.output/server/_libs/_45.mjs +2 -2
- package/.output/server/_libs/_46.mjs +2 -2
- package/.output/server/_libs/_47.mjs +2 -2
- package/.output/server/_libs/_48.mjs +2 -2
- package/.output/server/_libs/_49.mjs +2 -2
- package/.output/server/_libs/_5.mjs +2 -3
- package/.output/server/_libs/_50.mjs +2 -2
- package/.output/server/_libs/_51.mjs +2 -2
- package/.output/server/_libs/_52.mjs +2 -2
- package/.output/server/_libs/_53.mjs +2 -2
- package/.output/server/_libs/_54.mjs +2 -2
- package/.output/server/_libs/_55.mjs +2 -2
- package/.output/server/_libs/_56.mjs +2 -2
- package/.output/server/_libs/_57.mjs +2 -2
- package/.output/server/_libs/_58.mjs +2 -2
- package/.output/server/_libs/_59.mjs +2 -2
- package/.output/server/_libs/_6.mjs +2 -3
- package/.output/server/_libs/_60.mjs +2 -2
- package/.output/server/_libs/_61.mjs +2 -2
- package/.output/server/_libs/_62.mjs +2 -2
- package/.output/server/_libs/_63.mjs +2 -2
- package/.output/server/_libs/_64.mjs +2 -2
- package/.output/server/_libs/_65.mjs +2 -2
- package/.output/server/_libs/_66.mjs +2 -2
- package/.output/server/_libs/_67.mjs +2 -2
- package/.output/server/_libs/_68.mjs +2 -2
- package/.output/server/_libs/_69.mjs +2 -2
- package/.output/server/_libs/_7.mjs +2 -5
- package/.output/server/_libs/_70.mjs +2 -2
- package/.output/server/_libs/_71.mjs +2 -2
- package/.output/server/_libs/_72.mjs +2 -2
- package/.output/server/_libs/_73.mjs +2 -2
- package/.output/server/_libs/_74.mjs +2 -2
- package/.output/server/_libs/_75.mjs +2 -2
- package/.output/server/_libs/_76.mjs +2 -2
- package/.output/server/_libs/_77.mjs +2 -2
- package/.output/server/_libs/_78.mjs +2 -2
- package/.output/server/_libs/_79.mjs +2 -2
- package/.output/server/_libs/_8.mjs +2 -3
- package/.output/server/_libs/_80.mjs +2 -2
- package/.output/server/_libs/_81.mjs +2 -2
- package/.output/server/_libs/_82.mjs +2 -2
- package/.output/server/_libs/_83.mjs +2 -2
- package/.output/server/_libs/_84.mjs +2 -2
- package/.output/server/_libs/_85.mjs +2 -2
- package/.output/server/_libs/_86.mjs +2 -2
- package/.output/server/_libs/_87.mjs +2 -2
- package/.output/server/_libs/_88.mjs +2 -2
- package/.output/server/_libs/_89.mjs +2 -2
- package/.output/server/_libs/_9.mjs +2 -4
- package/.output/server/_libs/_90.mjs +5 -2
- package/.output/server/_libs/_91.mjs +3 -2
- package/.output/server/_libs/_92.mjs +2 -2
- package/.output/server/_libs/_93.mjs +2 -2
- package/.output/server/_libs/_94.mjs +2 -2
- package/.output/server/_libs/agent-base.mjs +1 -1
- package/.output/server/_libs/cheerio+[...].mjs +1 -1
- package/.output/server/_libs/data-uri-to-buffer.mjs +2 -67
- package/.output/server/_libs/data-urls+[...].mjs +1 -1
- package/.output/server/_libs/diff.mjs +1 -1
- package/.output/server/_libs/exodus__bytes.mjs +99 -81
- package/.output/server/_libs/fetch-blob+node-domexception.mjs +1 -1
- package/.output/server/_libs/h3+rou3+srvx.mjs +34 -4
- package/.output/server/_libs/html-encoding-sniffer.mjs +1 -1
- package/.output/server/_libs/https-proxy-agent.mjs +2 -2
- package/.output/server/_libs/jsdom.mjs +1 -1
- package/.output/server/_libs/just-bash+[...].mjs +4676 -3916
- package/.output/server/_libs/mariozechner__jiti.mjs +1 -1
- package/.output/server/_libs/mariozechner__pi-ai.mjs +1472 -0
- package/.output/server/_libs/md4x.mjs +1 -1
- package/.output/server/_libs/mime.mjs +838 -1
- package/.output/server/_libs/node-fetch.mjs +4 -4
- package/.output/server/_libs/node-liblzma.mjs +1 -1
- package/.output/server/_libs/silvia-odwyer__photon-node.mjs +1 -1
- package/.output/server/_routes/api/auth/approve.mjs +2 -0
- package/.output/server/_routes/api/auth/revoke.mjs +2 -0
- package/.output/server/_routes/api/auth/status.mjs +25 -6
- package/.output/server/_routes/api/browser2.mjs +1 -1
- package/.output/server/_routes/api/config2.mjs +2 -0
- package/.output/server/_routes/api/device_events.mjs +36 -0
- package/.output/server/_routes/api/files/groups.mjs +1 -2
- package/.output/server/_routes/api/files/raw.mjs +1 -1
- package/.output/server/_routes/api/groups.mjs +5 -3
- package/.output/server/_routes/api/groups2.mjs +18 -6
- package/.output/server/_routes/api/health.mjs +1 -2
- package/.output/server/_routes/api/messages.mjs +7 -1
- package/.output/server/_routes/api/notes/delete.mjs +4 -1
- package/.output/server/_routes/api/notes/write.mjs +2 -0
- package/.output/server/_routes/api/ntfy/setup.mjs +8 -0
- package/.output/server/_routes/api/pi/apikey.mjs +3 -2
- package/.output/server/_routes/api/pi/apikey_providers.mjs +1 -2
- package/.output/server/_routes/api/pi/commands.mjs +13 -3
- package/.output/server/_routes/api/pi/login/events.mjs +0 -1
- package/.output/server/_routes/api/pi/login/respond.mjs +2 -1
- package/.output/server/_routes/api/pi/login.mjs +1 -2
- package/.output/server/_routes/api/pi/logout.mjs +2 -1
- package/.output/server/_routes/api/pi/models.mjs +1 -2
- package/.output/server/_routes/api/pi/models_config2.mjs +2 -0
- package/.output/server/_routes/api/pi/settings2.mjs +2 -0
- package/.output/server/_routes/api/pi/status.mjs +1 -2
- package/.output/server/_routes/api/proxy.mjs +19 -1
- package/.output/server/_routes/api/sandbox.mjs +26 -0
- package/.output/server/_routes/api/sandbox2.mjs +17 -0
- package/.output/server/_routes/api/send.mjs +26 -18
- package/.output/server/_routes/api/status.mjs +1 -3
- package/.output/server/_routes/api/stop.mjs +11 -0
- package/.output/server/_routes/api/store/plugins.mjs +75 -0
- package/.output/server/_routes/api/store/skills.mjs +11 -0
- package/.output/server/_routes/api/tasks2.mjs +3 -2
- package/.output/server/_routes/api/telegram/setup.mjs +5 -2
- package/.output/server/_routes/api/telegram/status.mjs +1 -2
- package/.output/server/_routes/api/terminal2.mjs +2 -1
- package/.output/server/_routes/api/tunnel/setup.mjs +4 -2
- package/.output/server/_runtime.mjs +1 -2
- package/.output/server/_utils.mjs +10 -2
- package/.output/server/index.mjs +1 -1
- package/.output/server/node_modules/amdefine/amdefine.js +301 -0
- package/.output/server/node_modules/amdefine/package.json +16 -0
- package/.output/server/node_modules/compressjs/lib/BWT.js +420 -0
- package/.output/server/node_modules/compressjs/lib/BWTC.js +234 -0
- package/.output/server/node_modules/compressjs/lib/BitStream.js +108 -0
- package/.output/server/node_modules/compressjs/lib/Bzip2.js +936 -0
- package/.output/server/node_modules/compressjs/lib/CRC32.js +105 -0
- package/.output/server/node_modules/compressjs/lib/Context1Model.js +56 -0
- package/.output/server/node_modules/compressjs/lib/DefSumModel.js +152 -0
- package/.output/server/node_modules/compressjs/lib/DeflateDistanceModel.js +55 -0
- package/.output/server/node_modules/compressjs/lib/Dmc.js +197 -0
- package/.output/server/node_modules/compressjs/lib/DummyRangeCoder.js +81 -0
- package/.output/server/node_modules/compressjs/lib/FenwickModel.js +194 -0
- package/.output/server/node_modules/compressjs/lib/Huffman.js +514 -0
- package/.output/server/node_modules/compressjs/lib/HuffmanAllocator.js +227 -0
- package/.output/server/node_modules/compressjs/lib/LogDistanceModel.js +46 -0
- package/.output/server/node_modules/compressjs/lib/Lzjb.js +300 -0
- package/.output/server/node_modules/compressjs/lib/LzjbR.js +241 -0
- package/.output/server/node_modules/compressjs/lib/Lzp3.js +273 -0
- package/.output/server/node_modules/compressjs/lib/MTFModel.js +208 -0
- package/.output/server/node_modules/compressjs/lib/NoModel.js +46 -0
- package/.output/server/node_modules/compressjs/lib/PPM.js +343 -0
- package/.output/server/node_modules/compressjs/lib/RangeCoder.js +238 -0
- package/.output/server/node_modules/compressjs/lib/Simple.js +111 -0
- package/.output/server/node_modules/compressjs/lib/Stream.js +53 -0
- package/.output/server/node_modules/compressjs/lib/Util.js +324 -0
- package/.output/server/node_modules/compressjs/lib/freeze.js +14 -0
- package/.output/server/node_modules/compressjs/main.js +29 -0
- package/.output/server/node_modules/compressjs/package.json +35 -0
- package/.output/server/package.json +2 -1
- package/README.md +10 -1
- package/lib/index.d.mts +1 -0
- package/lib/index.mjs +1 -0
- package/lib/piclaw.mjs +100 -0
- package/lib/utils.mjs +96 -0
- package/package.json +16 -11
- package/.output/public/assets/defult-CMO6TZ5a.js +0 -1
- package/.output/public/assets/index-jdnbJw-M.js +0 -204
- package/.output/public/assets/index-ooXrRwgl.css +0 -1
- package/.output/server/_chunks/commands.mjs +0 -282
- package/.output/server/_chunks/pi.mjs +0 -202
- package/.output/server/_chunks/session.mjs +0 -1114
- package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +0 -299
- package/.output/server/_libs/@aws-sdk/client-bedrock-runtime+[...].mjs +0 -17828
- package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +0 -122
- package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +0 -417
- package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +0 -54
- package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +0 -1151
- package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +0 -50
- package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +0 -369
- package/.output/server/_libs/@tootallnate/quickjs-emscripten+[...].mjs +0 -3011
- package/.output/server/_libs/_100.mjs +0 -2
- package/.output/server/_libs/_101.mjs +0 -2
- package/.output/server/_libs/_102.mjs +0 -5
- package/.output/server/_libs/_103.mjs +0 -3
- package/.output/server/_libs/_104.mjs +0 -2
- package/.output/server/_libs/_105.mjs +0 -3
- package/.output/server/_libs/_106.mjs +0 -2
- package/.output/server/_libs/_107.mjs +0 -2
- package/.output/server/_libs/_95.mjs +0 -2
- package/.output/server/_libs/_96.mjs +0 -2
- package/.output/server/_libs/_97.mjs +0 -2
- package/.output/server/_libs/_98.mjs +0 -2
- package/.output/server/_libs/_99.mjs +0 -2
- package/.output/server/_libs/amdefine.mjs +0 -188
- package/.output/server/_libs/ast-types.mjs +0 -2270
- package/.output/server/_libs/aws-sdk__nested-clients.mjs +0 -3141
- package/.output/server/_libs/basic-ftp.mjs +0 -1906
- package/.output/server/_libs/compressjs.mjs +0 -50
- package/.output/server/_libs/degenerator+[...].mjs +0 -9964
- package/.output/server/_libs/get-uri.mjs +0 -413
- package/.output/server/_libs/http-proxy-agent.mjs +0 -123
- package/.output/server/_libs/ip-address.mjs +0 -1423
- package/.output/server/_libs/lru-cache.mjs +0 -732
- package/.output/server/_libs/netmask.mjs +0 -139
- package/.output/server/_libs/pac-proxy-agent+[...].mjs +0 -3104
- package/.output/server/_libs/proxy-agent+proxy-from-env.mjs +0 -204
- package/.output/server/_libs/smithy__core.mjs +0 -192
- package/.output/server/node_modules/tslib/modules/index.js +0 -70
- package/.output/server/node_modules/tslib/modules/package.json +0 -3
- package/.output/server/node_modules/tslib/package.json +0 -47
- package/.output/server/node_modules/tslib/tslib.js +0 -484
- package/bin/piclaw.mjs +0 -195
|
@@ -1,27 +1,1943 @@
|
|
|
1
|
+
import { o as __toESM, r as __exportAll } from "../_runtime.mjs";
|
|
1
2
|
import { i as SESSIONS_DIR, n as GROUPS_DIR, r as MAIN_GROUP_FOLDER, s as config } from "./config.mjs";
|
|
2
3
|
import { t as createLogger } from "./logger.mjs";
|
|
3
|
-
import { C as setSession, D as updateTaskAfterRun, S as setRouterState, T as storeMessage, _ as removeRegisteredGroup, b as setConfig, d as getMessagesSince, f as getNewMessages, g as logTaskRun, h as initDatabase, i as deleteConfig, l as getConfig, m as getTaskById, o as getAllRegisteredGroups, p as getRouterState, r as deleteChat, s as getAllSessions, u as getDueTasks, v as removeSession, w as storeChatMetadata, x as setRegisteredGroup, y as removeTasksByChat } from "./db.mjs";
|
|
4
|
+
import { C as setSession, D as updateTaskAfterRun, S as setRouterState, T as storeMessage, _ as removeRegisteredGroup, b as setConfig, c as getAllTasks, d as getMessagesSince, f as getNewMessages, g as logTaskRun, h as initDatabase, i as deleteConfig, l as getConfig, m as getTaskById, o as getAllRegisteredGroups, p as getRouterState, r as deleteChat, s as getAllSessions, t as clearMessages, u as getDueTasks, v as removeSession, w as storeChatMetadata, x as setRegisteredGroup, y as removeTasksByChat } from "./db.mjs";
|
|
5
|
+
import { a as createWriteTool, c as createBashTool, d as stripAnsi$1, i as AuthStorage, l as SessionManager, n as DefaultResourceLoader, o as createReadTool, r as ModelRegistry, s as createEditTool, t as createAgentSession, u as SettingsManager } from "../_libs/@mariozechner/pi-coding-agent+[...].mjs";
|
|
4
6
|
import { n as parseAST, t as init } from "../_libs/md4x.mjs";
|
|
5
7
|
import { t as streamBus } from "./stream.mjs";
|
|
6
|
-
import {
|
|
8
|
+
import { h as Type } from "../_libs/@mariozechner/pi-agent-core+[...].mjs";
|
|
9
|
+
import { t as deviceBus } from "./device-bus.mjs";
|
|
10
|
+
import { n as setBrowserState, t as getBrowserState } from "./browser.mjs";
|
|
11
|
+
import { i as writeNote, n as listNotes, r as readNote, t as deleteNote } from "./notes.mjs";
|
|
12
|
+
import { n as getNtfyConfig, r as sendNtfyNotification } from "./ntfy.mjs";
|
|
7
13
|
import { t as terminalManager } from "./terminal.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
14
|
+
import { t as sandboxManager } from "./sandbox.mjs";
|
|
15
|
+
import { a as saveAttachment, i as readAttachmentBase64, n as getMimeType, r as isImageMimeType } from "./uploads.mjs";
|
|
10
16
|
import { i as tunnelManager, n as getTunnelConfig, r as setTunnelConfig, t as clearTunnelConfig } from "./tunnel.mjs";
|
|
11
17
|
import fs from "fs";
|
|
12
18
|
import path from "path";
|
|
13
19
|
import fs$1 from "node:fs";
|
|
14
20
|
import path$1 from "node:path";
|
|
15
21
|
import crypto from "node:crypto";
|
|
22
|
+
import fs$2 from "node:fs/promises";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import "node:child_process";
|
|
25
|
+
function appendGroupEvent(folder, event) {
|
|
26
|
+
try {
|
|
27
|
+
const logsDir = path$1.join(GROUPS_DIR, folder, "logs");
|
|
28
|
+
fs$1.mkdirSync(logsDir, { recursive: true });
|
|
29
|
+
const line = JSON.stringify({
|
|
30
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31
|
+
...event
|
|
32
|
+
}) + "\n";
|
|
33
|
+
fs$1.appendFileSync(path$1.join(logsDir, "events.jsonl"), line, "utf-8");
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract readable text from a Pi SDK tool result.
|
|
38
|
+
* Handles `{ content: [{ type: "text", text }] }` objects, JSON strings, and plain strings.
|
|
39
|
+
* Strips ANSI/control chars and truncates.
|
|
40
|
+
*/
|
|
41
|
+
function extractToolResultText(value, maxLength = 200) {
|
|
42
|
+
let text = "";
|
|
43
|
+
if (typeof value === "string") try {
|
|
44
|
+
text = _textFromParsed(JSON.parse(value)) ?? value;
|
|
45
|
+
} catch {
|
|
46
|
+
text = value.match(/"text"\s*:\s*"((?:[^"\\]|\\.)*)(?:"|$)/)?.[1]?.replaceAll("\\n", "\n").replaceAll("\\t", " ").replaceAll("\\\"", "\"") ?? value;
|
|
47
|
+
}
|
|
48
|
+
else if (value && typeof value === "object") text = _textFromParsed(value) ?? JSON.stringify(value);
|
|
49
|
+
if (!text) return "";
|
|
50
|
+
text = stripAnsi$1(text).trim();
|
|
51
|
+
if (text.length > maxLength) return text.slice(0, maxLength) + "…";
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
function _textFromParsed(obj) {
|
|
55
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
56
|
+
const rec = obj;
|
|
57
|
+
if (Array.isArray(rec.content)) {
|
|
58
|
+
const first = rec.content[0];
|
|
59
|
+
if (first?.text && typeof first.text === "string") return first.text;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
var availableCommands;
|
|
63
|
+
async function getSystemInfo() {
|
|
64
|
+
const info = {
|
|
65
|
+
os: process.platform,
|
|
66
|
+
arch: process.arch,
|
|
67
|
+
nodeVersion: process.versions.node,
|
|
68
|
+
runtime: _getRuntime(),
|
|
69
|
+
freeMemory: os.freemem(),
|
|
70
|
+
totalMemory: os.totalmem(),
|
|
71
|
+
uptime: os.uptime()
|
|
72
|
+
};
|
|
73
|
+
if (process.platform === "linux") {
|
|
74
|
+
info.linuxDistro = await _getLinuxDistro();
|
|
75
|
+
info.isDocker = await _isDocker();
|
|
76
|
+
}
|
|
77
|
+
info.availableDiskSpace = await _getAvailableDiskSpace();
|
|
78
|
+
info.availableCommands = availableCommands ??= await _getAvailableCommands();
|
|
79
|
+
info.processMemory = process.memoryUsage().rss;
|
|
80
|
+
return info;
|
|
81
|
+
}
|
|
82
|
+
async function getSystemInfoText() {
|
|
83
|
+
const info = await getSystemInfo();
|
|
84
|
+
const lines = [];
|
|
85
|
+
let osLine = `${info.os} ${info.arch}`;
|
|
86
|
+
if (info.linuxDistro) osLine += ` (${info.linuxDistro})`;
|
|
87
|
+
if (info.isDocker) osLine += ` [Docker]`;
|
|
88
|
+
lines.push(`OS: ${osLine}`);
|
|
89
|
+
if (info.runtime) lines.push(`Runtime: ${info.runtime}`);
|
|
90
|
+
if (info.freeMemory != null && info.totalMemory != null) {
|
|
91
|
+
const fmt = (b) => `${(b / 1024 / 1024 / 1024).toFixed(1)}GB`;
|
|
92
|
+
let memLine = `Memory: ${fmt(info.freeMemory)} free / ${fmt(info.totalMemory)} total`;
|
|
93
|
+
if (info.processMemory != null) {
|
|
94
|
+
const mb = (info.processMemory / 1024 / 1024).toFixed(0);
|
|
95
|
+
memLine += ` (process: ${mb}MB)`;
|
|
96
|
+
}
|
|
97
|
+
lines.push(memLine);
|
|
98
|
+
}
|
|
99
|
+
if (info.availableDiskSpace != null) lines.push(`Disk: ${(info.availableDiskSpace / 1024 / 1024 / 1024).toFixed(1)}GB available`);
|
|
100
|
+
return lines.map((l) => `- ${l}`).join("\n");
|
|
101
|
+
}
|
|
102
|
+
function _getRuntime() {
|
|
103
|
+
if ("Bun" in globalThis) return `bun v${globalThis.Bun.version}`;
|
|
104
|
+
if ("Deno" in globalThis) return `deno v${globalThis.Deno.version.deno}`;
|
|
105
|
+
return `node v${process.versions.node}`;
|
|
106
|
+
}
|
|
107
|
+
async function _getLinuxDistro() {
|
|
108
|
+
try {
|
|
109
|
+
return (await fs$2.readFile("/etc/os-release", "utf8")).match(/^PRETTY_NAME="?(.+?)"?\s*$/m)?.[1] ?? null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function _isDocker() {
|
|
115
|
+
try {
|
|
116
|
+
await fs$2.access("/.dockerenv");
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
try {
|
|
120
|
+
const cgroup = await fs$2.readFile("/proc/1/cgroup", "utf8");
|
|
121
|
+
return cgroup.includes("docker") || cgroup.includes("containerd");
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function _getAvailableCommands() {
|
|
128
|
+
try {
|
|
129
|
+
const pathEnv = process.env.PATH;
|
|
130
|
+
if (!pathEnv) return void 0;
|
|
131
|
+
const dirs = [...new Set(pathEnv.split(":"))];
|
|
132
|
+
const commands = /* @__PURE__ */ new Set();
|
|
133
|
+
await Promise.all(dirs.map(async (dir) => {
|
|
134
|
+
try {
|
|
135
|
+
const entries = await fs$2.readdir(dir);
|
|
136
|
+
for (const entry of entries) commands.add(entry);
|
|
137
|
+
} catch {}
|
|
138
|
+
}));
|
|
139
|
+
return [...commands].sort();
|
|
140
|
+
} catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function _getAvailableDiskSpace() {
|
|
145
|
+
try {
|
|
146
|
+
const stats = await fs$2.statfs("/");
|
|
147
|
+
return stats.bsize * stats.bavail;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var logger$6 = createLogger("pi");
|
|
153
|
+
function sendPrompt(managed, group, input, onOutput) {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const { session } = managed;
|
|
156
|
+
let fullText = "";
|
|
157
|
+
let thinkingText = "";
|
|
158
|
+
let resolved = false;
|
|
159
|
+
let flushedTextLength = 0;
|
|
160
|
+
/** Chronologically ordered raw items (thinking text + tool markers) flushed as single toggle */
|
|
161
|
+
const pendingItems = [];
|
|
162
|
+
let outputChain = Promise.resolve();
|
|
163
|
+
const timeout = setTimeout(() => {
|
|
164
|
+
if (resolved) return;
|
|
165
|
+
resolved = true;
|
|
166
|
+
logger$6.error({ group: group.name }, "Agent timeout");
|
|
167
|
+
appendGroupEvent(group.folder, {
|
|
168
|
+
type: "error",
|
|
169
|
+
reason: "timeout"
|
|
170
|
+
});
|
|
171
|
+
streamBus.emit({
|
|
172
|
+
type: "error",
|
|
173
|
+
chatJid: input.chatJid,
|
|
174
|
+
error: "Agent timeout"
|
|
175
|
+
});
|
|
176
|
+
session.abort().catch(() => {});
|
|
177
|
+
resolve({
|
|
178
|
+
status: "error",
|
|
179
|
+
result: fullText || null,
|
|
180
|
+
newSessionId: session.sessionId,
|
|
181
|
+
error: "Agent timeout"
|
|
182
|
+
});
|
|
183
|
+
}, config.get("agentTimeout"));
|
|
184
|
+
const finish = (status, error) => {
|
|
185
|
+
if (resolved) return;
|
|
186
|
+
resolved = true;
|
|
187
|
+
clearTimeout(timeout);
|
|
188
|
+
clearInterval(abortCheck);
|
|
189
|
+
outputChain.then(() => {
|
|
190
|
+
resolveAbortDone?.();
|
|
191
|
+
resolve({
|
|
192
|
+
status,
|
|
193
|
+
result: fullText || null,
|
|
194
|
+
newSessionId: session.sessionId,
|
|
195
|
+
error
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
let unsubscribe;
|
|
200
|
+
let resolveAbortDone;
|
|
201
|
+
managed.abortDone = new Promise((r) => {
|
|
202
|
+
resolveAbortDone = r;
|
|
203
|
+
});
|
|
204
|
+
const abortCheck = setInterval(() => {
|
|
205
|
+
if (managed.aborted && !resolved) {
|
|
206
|
+
clearInterval(abortCheck);
|
|
207
|
+
unsubscribe?.();
|
|
208
|
+
const unflushed = fullText.slice(flushedTextLength);
|
|
209
|
+
const jid = input.chatJid;
|
|
210
|
+
if (onOutput) {
|
|
211
|
+
const parts = [];
|
|
212
|
+
if (thinkingText) {
|
|
213
|
+
pendingItems.push(thinkingText);
|
|
214
|
+
thinkingText = "";
|
|
215
|
+
}
|
|
216
|
+
if (pendingItems.length) parts.push(flushPendingItems(pendingItems));
|
|
217
|
+
if (unflushed) parts.push(unflushed);
|
|
218
|
+
const partial = parts.length ? parts.join("\n\n") : null;
|
|
219
|
+
outputChain = outputChain.then(async () => {
|
|
220
|
+
await onOutput({
|
|
221
|
+
status: "success",
|
|
222
|
+
result: partial ? partial + "\n\n_(stopped)_" : "_(stopped)_",
|
|
223
|
+
newSessionId: session.sessionId
|
|
224
|
+
});
|
|
225
|
+
if (partial) streamBus.emit({
|
|
226
|
+
type: "text_end",
|
|
227
|
+
chatJid: jid,
|
|
228
|
+
content: partial
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
outputChain = outputChain.then(() => {
|
|
233
|
+
streamBus.emit({
|
|
234
|
+
type: "agent_end",
|
|
235
|
+
chatJid: jid
|
|
236
|
+
});
|
|
237
|
+
resolveAbortDone?.();
|
|
238
|
+
});
|
|
239
|
+
finish("stopped", "Session stopped by user");
|
|
240
|
+
}
|
|
241
|
+
}, 200);
|
|
242
|
+
unsubscribe = session.subscribe((event) => {
|
|
243
|
+
if (resolved) {
|
|
244
|
+
clearInterval(abortCheck);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const jid = input.chatJid;
|
|
248
|
+
switch (event.type) {
|
|
249
|
+
case "message_update": {
|
|
250
|
+
const ae = event.assistantMessageEvent;
|
|
251
|
+
if (ae.type === "text_delta" && "delta" in ae) {
|
|
252
|
+
fullText += ae.delta;
|
|
253
|
+
streamBus.emit({
|
|
254
|
+
type: "text_delta",
|
|
255
|
+
chatJid: jid,
|
|
256
|
+
delta: ae.delta
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (ae.type === "text_end" && "content" in ae && onOutput) {
|
|
260
|
+
const text = ae.content;
|
|
261
|
+
const parts = [];
|
|
262
|
+
if (thinkingText) {
|
|
263
|
+
pendingItems.push(thinkingText);
|
|
264
|
+
thinkingText = "";
|
|
265
|
+
}
|
|
266
|
+
if (pendingItems.length) parts.push(flushPendingItems(pendingItems));
|
|
267
|
+
parts.push(text);
|
|
268
|
+
const stored = parts.join("\n\n");
|
|
269
|
+
appendGroupEvent(group.folder, {
|
|
270
|
+
type: "text_end",
|
|
271
|
+
length: stored.length,
|
|
272
|
+
preview: stored.slice(0, 100)
|
|
273
|
+
});
|
|
274
|
+
outputChain = outputChain.then(async () => {
|
|
275
|
+
await onOutput({
|
|
276
|
+
status: "success",
|
|
277
|
+
result: stored,
|
|
278
|
+
newSessionId: session.sessionId
|
|
279
|
+
});
|
|
280
|
+
flushedTextLength = fullText.length;
|
|
281
|
+
streamBus.emit({
|
|
282
|
+
type: "text_end",
|
|
283
|
+
chatJid: jid,
|
|
284
|
+
content: stored
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (ae.type === "thinking_delta" && "delta" in ae) {
|
|
289
|
+
thinkingText += ae.delta;
|
|
290
|
+
streamBus.emit({
|
|
291
|
+
type: "thinking_delta",
|
|
292
|
+
chatJid: jid,
|
|
293
|
+
delta: ae.delta
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (ae.type === "thinking_end" && "content" in ae) {
|
|
297
|
+
if (thinkingText) {
|
|
298
|
+
pendingItems.push(thinkingText);
|
|
299
|
+
thinkingText = "";
|
|
300
|
+
}
|
|
301
|
+
streamBus.emit({
|
|
302
|
+
type: "thinking_end",
|
|
303
|
+
chatJid: jid,
|
|
304
|
+
content: ae.content
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
case "agent_end":
|
|
310
|
+
unsubscribe();
|
|
311
|
+
appendGroupEvent(group.folder, { type: "agent_end" });
|
|
312
|
+
streamBus.emit({
|
|
313
|
+
type: "agent_end",
|
|
314
|
+
chatJid: jid
|
|
315
|
+
});
|
|
316
|
+
finish("success");
|
|
317
|
+
break;
|
|
318
|
+
case "tool_execution_start":
|
|
319
|
+
logger$6.debug({
|
|
320
|
+
group: group.name,
|
|
321
|
+
tool: event.toolName,
|
|
322
|
+
toolCallId: event.toolCallId,
|
|
323
|
+
argsPreview: previewForLog(event.args)
|
|
324
|
+
}, "Tool execution started");
|
|
325
|
+
appendGroupEvent(group.folder, {
|
|
326
|
+
type: "tool_start",
|
|
327
|
+
tool: event.toolName,
|
|
328
|
+
toolCallId: event.toolCallId,
|
|
329
|
+
argsPreview: previewForLog(event.args, 200)
|
|
330
|
+
});
|
|
331
|
+
streamBus.emit({
|
|
332
|
+
type: "tool_start",
|
|
333
|
+
chatJid: jid,
|
|
334
|
+
toolName: event.toolName
|
|
335
|
+
});
|
|
336
|
+
break;
|
|
337
|
+
case "tool_execution_update":
|
|
338
|
+
logger$6.debug({
|
|
339
|
+
group: group.name,
|
|
340
|
+
tool: event.toolName,
|
|
341
|
+
toolCallId: event.toolCallId,
|
|
342
|
+
partialResultPreview: previewForLog(event.partialResult)
|
|
343
|
+
}, "Tool execution update");
|
|
344
|
+
break;
|
|
345
|
+
case "tool_execution_end": {
|
|
346
|
+
const payload = {
|
|
347
|
+
group: group.name,
|
|
348
|
+
tool: event.toolName,
|
|
349
|
+
toolCallId: event.toolCallId,
|
|
350
|
+
isError: event.isError,
|
|
351
|
+
resultPreview: previewForLog(event.result)
|
|
352
|
+
};
|
|
353
|
+
if (event.isError) logger$6.debug(payload, "Tool execution error");
|
|
354
|
+
else logger$6.debug(payload, "Tool execution finished");
|
|
355
|
+
appendGroupEvent(group.folder, {
|
|
356
|
+
type: "tool_end",
|
|
357
|
+
tool: event.toolName,
|
|
358
|
+
toolCallId: event.toolCallId,
|
|
359
|
+
isError: event.isError,
|
|
360
|
+
resultPreview: previewForLog(event.result, 200)
|
|
361
|
+
});
|
|
362
|
+
const cleanPreview = extractToolResultText(event.result) || void 0;
|
|
363
|
+
pendingItems.push(toToolMarker(event.toolName, event.isError, cleanPreview));
|
|
364
|
+
streamBus.emit({
|
|
365
|
+
type: "tool_end",
|
|
366
|
+
chatJid: jid,
|
|
367
|
+
toolName: event.toolName,
|
|
368
|
+
isError: event.isError,
|
|
369
|
+
resultPreview: cleanPreview
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
session.prompt(input.prompt, { images: input.images?.length ? input.images : void 0 }).catch((err) => {
|
|
376
|
+
unsubscribe();
|
|
377
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
378
|
+
logger$6.error({
|
|
379
|
+
group: group.name,
|
|
380
|
+
err
|
|
381
|
+
}, "Prompt error");
|
|
382
|
+
appendGroupEvent(group.folder, {
|
|
383
|
+
type: "error",
|
|
384
|
+
reason
|
|
385
|
+
});
|
|
386
|
+
streamBus.emit({
|
|
387
|
+
type: "error",
|
|
388
|
+
chatJid: input.chatJid,
|
|
389
|
+
error: reason
|
|
390
|
+
});
|
|
391
|
+
finish("error", reason);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
var baseSystemPrompt = `
|
|
396
|
+
You live in your own OS and help your human with tasks.
|
|
397
|
+
|
|
398
|
+
Available tools:
|
|
399
|
+
- read: Read file contents
|
|
400
|
+
- bash: Execute shell commands (ls, rg, find, curl, etc.)
|
|
401
|
+
- edit: Make precise text replacements in existing files
|
|
402
|
+
- write: Create or fully overwrite files
|
|
403
|
+
- notes: Read/write/list/delete markdown notes by category — shared across all agents
|
|
404
|
+
- notify: Send notifications to user
|
|
405
|
+
- terminal: Manage persistent terminal sessions with pty support
|
|
406
|
+
- browser: Open, navigate, switch, and close browser tabs in the admin UI
|
|
407
|
+
- message: Send a message to another group's agent by folder name
|
|
408
|
+
|
|
409
|
+
Execution rules:
|
|
410
|
+
- You operate directly in your own OS/workspace environment autonomously; you do not need to ask for permission to run bash commands; users do not know how to run commands you do it for them.
|
|
411
|
+
- Use tools proactively to do the work; do not just describe what should be done.
|
|
412
|
+
- Use bash for operations (searching, building, testing, git-aware inspection).
|
|
413
|
+
- Use terminal for long running processes such as dev server or tmux.
|
|
414
|
+
- Do not use cat/sed/awk to read files use read tool instead.
|
|
415
|
+
- IMPORTANT: Always read a file before editing it. The edit tool requires an exact match of existing text — if you guess the content, it will fail. Read first, then copy the exact text to replace.
|
|
416
|
+
- Open browser tabs only when explicitly asked.
|
|
417
|
+
- Prefer edit for targeted changes; use write for new files or full rewrites.
|
|
418
|
+
- Use notify tool as a secondary channel to send important info to the user and for anything you can't send in chat. Do not repeat chat messages in notifications.
|
|
419
|
+
- Summarize results in plain text; do not print summaries via bash commands.
|
|
420
|
+
- Be concise, explicit, and include file paths when referencing changes.
|
|
421
|
+
- For thinking/planning notes in your response, use blockquote syntax (> ...) not bold (**...**). Example: "> Planning server conversion steps" instead of "**Planning server conversion steps**".
|
|
422
|
+
- Ask a short clarification question when requirements are ambiguous or risky.
|
|
423
|
+
- never say "Use this command", you can just execute commands using bash tool.
|
|
424
|
+
- always write necessary info to AGENTS.md, it is your own private memory (do not ask user to do it).
|
|
425
|
+
- Keep AGENTS.md concise: when it gets long, compact older entries into brief summaries while preserving key decisions, current state, and open tasks.
|
|
426
|
+
- Do not mention AGENTS.md updates in normal replies unless the user explicitly asks about memory/log updates.
|
|
427
|
+
- Use notes tool for persistent, non-task-specific information that should be shared across all agents (e.g. user preferences, reference material, reusable snippets). AGENTS.md is for your private context; notes are global memory.
|
|
428
|
+
`;
|
|
429
|
+
async function buildSystemPrompt(ctx) {
|
|
430
|
+
let prompt = `${ctx.soul ? ctx.soul.trim() : `You are ${ctx.assistantName}, an expert assistant.`}
|
|
431
|
+
|
|
432
|
+
${baseSystemPrompt}
|
|
433
|
+
|
|
434
|
+
Current model name: ${ctx.modelName}
|
|
435
|
+
`;
|
|
436
|
+
const envInfo = await getSystemInfoText().catch((err) => {
|
|
437
|
+
logger$6.debug({ err }, "Failed to get system info");
|
|
438
|
+
return null;
|
|
439
|
+
});
|
|
440
|
+
if (envInfo) prompt += `\n\nEnvironment:\n${envInfo}`;
|
|
441
|
+
return prompt;
|
|
442
|
+
}
|
|
443
|
+
function previewForLog(value, maxLength = 1200) {
|
|
444
|
+
try {
|
|
445
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
446
|
+
if (!text) return "";
|
|
447
|
+
if (text.length <= maxLength) return text;
|
|
448
|
+
return `${text.slice(0, maxLength)}…(truncated ${text.length - maxLength} chars)`;
|
|
449
|
+
} catch {
|
|
450
|
+
return "[unserializable value]";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/** Flush all pending items (thinking text + tool markers) into a single collapsible block */
|
|
454
|
+
function flushPendingItems(items) {
|
|
455
|
+
return `<details><summary>thinking</summary>\n\n${items.splice(0).join("\n\n")}\n\n</details>`;
|
|
456
|
+
}
|
|
457
|
+
/** Build an inline tool marker tag */
|
|
458
|
+
function toToolMarker(name, isError, preview) {
|
|
459
|
+
return `<tool name="${name}" status="${isError ? "error" : "ok"}"${preview ? ` preview="${preview.replaceAll("\"", """)}"` : ""}/>`;
|
|
460
|
+
}
|
|
461
|
+
const browserTool = {
|
|
462
|
+
name: "browser",
|
|
463
|
+
label: "Browser",
|
|
464
|
+
description: [
|
|
465
|
+
"Control the browser window tabs.",
|
|
466
|
+
"Actions: list (show open tabs and active tab),",
|
|
467
|
+
"open (open a new tab, optionally with a URL),",
|
|
468
|
+
"navigate (change URL of a tab), switch (activate a tab), close (close a tab)."
|
|
469
|
+
].join(" "),
|
|
470
|
+
promptSnippet: "browser: Open, navigate, switch, and close browser tabs in the admin UI",
|
|
471
|
+
promptGuidelines: [
|
|
472
|
+
"Use the browser tool to open web pages in the admin browser window.",
|
|
473
|
+
"Use list to see current tabs before operating on them.",
|
|
474
|
+
"Tab IDs are integers — get them from list output."
|
|
475
|
+
],
|
|
476
|
+
parameters: Type.Object({
|
|
477
|
+
action: Type.Union([
|
|
478
|
+
Type.Literal("list"),
|
|
479
|
+
Type.Literal("open"),
|
|
480
|
+
Type.Literal("close"),
|
|
481
|
+
Type.Literal("navigate"),
|
|
482
|
+
Type.Literal("switch")
|
|
483
|
+
], { description: "Action to perform" }),
|
|
484
|
+
url: Type.Optional(Type.String({ description: "URL for open/navigate actions (http/https)" })),
|
|
485
|
+
tabId: Type.Optional(Type.Number({ description: "Tab ID for close/navigate/switch actions" }))
|
|
486
|
+
}),
|
|
487
|
+
async execute(_toolCallId, params) {
|
|
488
|
+
const p = params;
|
|
489
|
+
switch (p.action) {
|
|
490
|
+
case "list": return handleList$2();
|
|
491
|
+
case "open": return handleOpen(p);
|
|
492
|
+
case "close": return handleClose(p);
|
|
493
|
+
case "navigate": return handleNavigate(p);
|
|
494
|
+
case "switch": return handleSwitch(p);
|
|
495
|
+
default: return err$4(`Unknown action: ${p.action}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
function handleList$2() {
|
|
500
|
+
const state = getBrowserState();
|
|
501
|
+
if (state.tabs.length === 0) return ok$3("No browser tabs open.");
|
|
502
|
+
return ok$3(`Browser tabs:\n${state.tabs.map((t) => `- [${t.id}]${t.id === state.activeTabId ? " (active)" : ""} ${t.url || "(empty)"}`).join("\n")}`, {
|
|
503
|
+
tabs: state.tabs,
|
|
504
|
+
activeTabId: state.activeTabId
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
function handleOpen(p) {
|
|
508
|
+
const state = getBrowserState();
|
|
509
|
+
const id = Math.max(0, ...state.tabs.map((t) => t.id)) + 1;
|
|
510
|
+
setBrowserState({
|
|
511
|
+
tabs: [...state.tabs, {
|
|
512
|
+
id,
|
|
513
|
+
url: p.url || ""
|
|
514
|
+
}],
|
|
515
|
+
activeTabId: id
|
|
516
|
+
});
|
|
517
|
+
return ok$3(`Opened tab ${id}${p.url ? ` at ${p.url}` : ""}.`, { tabId: id });
|
|
518
|
+
}
|
|
519
|
+
function handleClose(p) {
|
|
520
|
+
if (p.tabId == null) return err$4("tabId is required for close");
|
|
521
|
+
const state = getBrowserState();
|
|
522
|
+
const idx = state.tabs.findIndex((t) => t.id === p.tabId);
|
|
523
|
+
if (idx < 0) return err$4(`Tab ${p.tabId} not found`);
|
|
524
|
+
const tabs = state.tabs.filter((t) => t.id !== p.tabId);
|
|
525
|
+
let activeTabId = state.activeTabId;
|
|
526
|
+
if (activeTabId === p.tabId) activeTabId = tabs[Math.min(idx, tabs.length - 1)]?.id ?? null;
|
|
527
|
+
setBrowserState({
|
|
528
|
+
tabs,
|
|
529
|
+
activeTabId
|
|
530
|
+
});
|
|
531
|
+
return ok$3(`Closed tab ${p.tabId}.`, { tabId: p.tabId });
|
|
532
|
+
}
|
|
533
|
+
function handleNavigate(p) {
|
|
534
|
+
if (p.tabId == null) return err$4("tabId is required for navigate");
|
|
535
|
+
if (!p.url) return err$4("url is required for navigate");
|
|
536
|
+
const state = getBrowserState();
|
|
537
|
+
if (!state.tabs.find((t) => t.id === p.tabId)) return err$4(`Tab ${p.tabId} not found`);
|
|
538
|
+
setBrowserState({
|
|
539
|
+
tabs: state.tabs.map((t) => t.id === p.tabId ? {
|
|
540
|
+
...t,
|
|
541
|
+
url: p.url
|
|
542
|
+
} : t),
|
|
543
|
+
activeTabId: state.activeTabId
|
|
544
|
+
});
|
|
545
|
+
return ok$3(`Navigated tab ${p.tabId} to ${p.url}.`, {
|
|
546
|
+
tabId: p.tabId,
|
|
547
|
+
url: p.url
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
function handleSwitch(p) {
|
|
551
|
+
if (p.tabId == null) return err$4("tabId is required for switch");
|
|
552
|
+
const state = getBrowserState();
|
|
553
|
+
if (!state.tabs.some((t) => t.id === p.tabId)) return err$4(`Tab ${p.tabId} not found`);
|
|
554
|
+
setBrowserState({
|
|
555
|
+
tabs: state.tabs,
|
|
556
|
+
activeTabId: p.tabId
|
|
557
|
+
});
|
|
558
|
+
return ok$3(`Switched to tab ${p.tabId}.`, { tabId: p.tabId });
|
|
559
|
+
}
|
|
560
|
+
function ok$3(text, details) {
|
|
561
|
+
return {
|
|
562
|
+
content: [{
|
|
563
|
+
type: "text",
|
|
564
|
+
text
|
|
565
|
+
}],
|
|
566
|
+
details: {
|
|
567
|
+
ok: true,
|
|
568
|
+
...details
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function err$4(text) {
|
|
573
|
+
return {
|
|
574
|
+
content: [{
|
|
575
|
+
type: "text",
|
|
576
|
+
text
|
|
577
|
+
}],
|
|
578
|
+
details: {
|
|
579
|
+
ok: false,
|
|
580
|
+
isError: true
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function createMessageTool(callerGroupFolder) {
|
|
585
|
+
return {
|
|
586
|
+
name: "message",
|
|
587
|
+
label: "Message",
|
|
588
|
+
description: [
|
|
589
|
+
"Send a message to another registered group's agent.",
|
|
590
|
+
"The message appears in the target group's chat and triggers the target agent to process it.",
|
|
591
|
+
"Use 'folder' to identify the target group."
|
|
592
|
+
].join(" "),
|
|
593
|
+
promptSnippet: "message: Send a message to another group's agent by folder name",
|
|
594
|
+
promptGuidelines: [
|
|
595
|
+
"Use 'message' to delegate work or coordinate with another group's agent.",
|
|
596
|
+
"Specify the 'folder' (not display name) of the target group.",
|
|
597
|
+
"The receiving agent sees the message as if it came from you."
|
|
598
|
+
],
|
|
599
|
+
parameters: Type.Object({
|
|
600
|
+
folder: Type.String({ description: "Target group folder name" }),
|
|
601
|
+
content: Type.String({ description: "Message content to send" })
|
|
602
|
+
}),
|
|
603
|
+
async execute(_toolCallId, params) {
|
|
604
|
+
const p = params;
|
|
605
|
+
if (!server) return err$3("Server not available");
|
|
606
|
+
const groups = server.getRegisteredGroups();
|
|
607
|
+
const targetEntry = Object.entries(groups).find(([, g]) => g.folder === p.folder);
|
|
608
|
+
if (!targetEntry) {
|
|
609
|
+
const available = Object.values(groups).map((g) => `${g.name} (${g.folder})`).join(", ");
|
|
610
|
+
return err$3(`Group folder "${p.folder}" not found. Available: ${available}`);
|
|
611
|
+
}
|
|
612
|
+
const [targetJid, targetGroup] = targetEntry;
|
|
613
|
+
const senderLabel = `agent:${callerGroupFolder}`;
|
|
614
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
615
|
+
await storeChatMetadata(targetJid, now, targetGroup.name);
|
|
616
|
+
await storeMessage({
|
|
617
|
+
id: crypto.randomUUID(),
|
|
618
|
+
chat_jid: targetJid,
|
|
619
|
+
sender: senderLabel,
|
|
620
|
+
sender_name: senderLabel,
|
|
621
|
+
content: p.content,
|
|
622
|
+
timestamp: now,
|
|
623
|
+
is_from_me: false,
|
|
624
|
+
is_bot_message: false
|
|
625
|
+
});
|
|
626
|
+
server.queue.enqueueMessageCheck(targetJid);
|
|
627
|
+
return {
|
|
628
|
+
content: [{
|
|
629
|
+
type: "text",
|
|
630
|
+
text: `Message sent to "${targetGroup.name}" (${p.folder})`
|
|
631
|
+
}],
|
|
632
|
+
details: {
|
|
633
|
+
ok: true,
|
|
634
|
+
targetJid,
|
|
635
|
+
folder: p.folder
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function err$3(text) {
|
|
642
|
+
return {
|
|
643
|
+
content: [{
|
|
644
|
+
type: "text",
|
|
645
|
+
text
|
|
646
|
+
}],
|
|
647
|
+
details: {
|
|
648
|
+
ok: false,
|
|
649
|
+
isError: true
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
const notesTool = {
|
|
654
|
+
name: "notes",
|
|
655
|
+
label: "Notes",
|
|
656
|
+
description: [
|
|
657
|
+
"Manage markdown notes organized by category.",
|
|
658
|
+
"Actions: list (show notes, optionally filtered by category),",
|
|
659
|
+
"read (get note content), write (create/overwrite a note), delete (remove a note).",
|
|
660
|
+
"Notes are referenced as \"category/id\" (e.g. \"ideas/project-plan\").",
|
|
661
|
+
"Virtual ~agents notes map to per-session AGENTS.md files."
|
|
662
|
+
].join(" "),
|
|
663
|
+
promptSnippet: "notes: Read, write, list, and delete markdown notes organized by category",
|
|
664
|
+
promptGuidelines: [
|
|
665
|
+
"Use notes to persist structured information across sessions — plans, references, logs, etc.",
|
|
666
|
+
"Note refs use \"category/id\" format, e.g. \"tasks/backlog\" or \"docs/api-spec\".",
|
|
667
|
+
"List notes first to see what exists before reading or writing.",
|
|
668
|
+
"Virtual ~agents/ notes map to per-session AGENTS.md files."
|
|
669
|
+
],
|
|
670
|
+
parameters: Type.Object({
|
|
671
|
+
action: Type.Union([
|
|
672
|
+
Type.Literal("list"),
|
|
673
|
+
Type.Literal("read"),
|
|
674
|
+
Type.Literal("write"),
|
|
675
|
+
Type.Literal("delete")
|
|
676
|
+
], { description: "Action to perform" }),
|
|
677
|
+
ref: Type.Optional(Type.String({ description: "Note reference as \"category/id\" (required for read/write/delete)" })),
|
|
678
|
+
category: Type.Optional(Type.String({ description: "Filter by category (list only)" })),
|
|
679
|
+
content: Type.Optional(Type.String({ description: "Markdown content to write (write only)" }))
|
|
680
|
+
}),
|
|
681
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
682
|
+
const p = params;
|
|
683
|
+
switch (p.action) {
|
|
684
|
+
case "list": return handleList$1(p);
|
|
685
|
+
case "read": return handleRead$1(p);
|
|
686
|
+
case "write": return handleWrite$1(p);
|
|
687
|
+
case "delete": return handleDelete(p);
|
|
688
|
+
default: return err$2(`Unknown action: ${p.action}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
function handleList$1(p) {
|
|
693
|
+
const entries = listNotes(p.category);
|
|
694
|
+
if (entries.length === 0) return ok$2(p.category ? `No notes in category "${p.category}".` : "No notes found.");
|
|
695
|
+
const lines = entries.map((e) => `- ${e.category}/${e.id}`);
|
|
696
|
+
return ok$2(`Notes (${entries.length}):\n${lines.join("\n")}`, { entries });
|
|
697
|
+
}
|
|
698
|
+
function handleRead$1(p) {
|
|
699
|
+
if (!p.ref) return err$2("ref is required for read");
|
|
700
|
+
const content = readNote(p.ref);
|
|
701
|
+
if (content === void 0) return err$2(`Note "${p.ref}" not found`);
|
|
702
|
+
return ok$2(content, { ref: p.ref });
|
|
703
|
+
}
|
|
704
|
+
function handleWrite$1(p) {
|
|
705
|
+
if (!p.ref) return err$2("ref is required for write");
|
|
706
|
+
if (p.content == null) return err$2("content is required for write");
|
|
707
|
+
try {
|
|
708
|
+
writeNote(p.ref, p.content);
|
|
709
|
+
deviceBus.emitDeviceEvent("notes");
|
|
710
|
+
return ok$2(`Note "${p.ref}" saved.`, { ref: p.ref });
|
|
711
|
+
} catch (e) {
|
|
712
|
+
return err$2(`Failed to write note: ${e instanceof Error ? e.message : String(e)}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function handleDelete(p) {
|
|
716
|
+
if (!p.ref) return err$2("ref is required for delete");
|
|
717
|
+
if (!deleteNote(p.ref)) return err$2(`Note "${p.ref}" not found or cannot be deleted`);
|
|
718
|
+
deviceBus.emitDeviceEvent("notes");
|
|
719
|
+
return ok$2(`Note "${p.ref}" deleted.`, { ref: p.ref });
|
|
720
|
+
}
|
|
721
|
+
function ok$2(text, details) {
|
|
722
|
+
return {
|
|
723
|
+
content: [{
|
|
724
|
+
type: "text",
|
|
725
|
+
text
|
|
726
|
+
}],
|
|
727
|
+
details: {
|
|
728
|
+
ok: true,
|
|
729
|
+
...details
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function err$2(text) {
|
|
734
|
+
return {
|
|
735
|
+
content: [{
|
|
736
|
+
type: "text",
|
|
737
|
+
text
|
|
738
|
+
}],
|
|
739
|
+
details: {
|
|
740
|
+
ok: false,
|
|
741
|
+
isError: true
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
const notifyTool = {
|
|
746
|
+
name: "notify",
|
|
747
|
+
label: "Notify",
|
|
748
|
+
description: [
|
|
749
|
+
"Send a notification to the user. Always sends an in-app toast notification to connected browsers.",
|
|
750
|
+
"If ntfy is configured, also sends a push notification via ntfy with full ntfy features",
|
|
751
|
+
"(priority, emoji tags, clickable URLs, markdown, scheduled delivery, action buttons, attachments)."
|
|
752
|
+
].join(" "),
|
|
753
|
+
parameters: Type.Object({
|
|
754
|
+
message: Type.String({ description: "Notification body text" }),
|
|
755
|
+
title: Type.Optional(Type.String({ description: "Notification title" })),
|
|
756
|
+
priority: Type.Optional(Type.Union([
|
|
757
|
+
Type.Literal(1),
|
|
758
|
+
Type.Literal(2),
|
|
759
|
+
Type.Literal(3),
|
|
760
|
+
Type.Literal(4),
|
|
761
|
+
Type.Literal(5)
|
|
762
|
+
], { description: "1=min, 2=low, 3=default, 4=high, 5=urgent" })),
|
|
763
|
+
tags: Type.Optional(Type.Array(Type.String(), { description: "Emoji shortcodes or labels, e.g. [\"white_check_mark\",\"robot\"]. See https://docs.ntfy.sh/emojis/" })),
|
|
764
|
+
click: Type.Optional(Type.String({ description: "URL opened when notification is tapped" })),
|
|
765
|
+
markdown: Type.Optional(Type.Boolean({ description: "Enable markdown formatting in body (enabled by default)" })),
|
|
766
|
+
delay: Type.Optional(Type.String({ description: "Schedule delivery: duration (\"30m\",\"2h\"), timestamp, or \"tomorrow 9am\" (cannot be combined with email)" })),
|
|
767
|
+
actions: Type.Optional(Type.Array(Type.String(), { description: "Action buttons (max 3). Format: \"view, Label, https://...\" or \"http, Label, https://...\"" })),
|
|
768
|
+
attach: Type.Optional(Type.String({ description: "URL to an external file to attach" })),
|
|
769
|
+
filename: Type.Optional(Type.String({ description: "Override attachment filename" })),
|
|
770
|
+
email: Type.Optional(Type.String({ description: "Also forward notification to this email" }))
|
|
771
|
+
}),
|
|
772
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
773
|
+
const p = params;
|
|
774
|
+
deviceBus.notify({
|
|
775
|
+
title: p.title ?? p.message,
|
|
776
|
+
description: p.title ? p.message : void 0,
|
|
777
|
+
type: p.priority ? {
|
|
778
|
+
5: "alert",
|
|
779
|
+
4: "alert",
|
|
780
|
+
3: "info",
|
|
781
|
+
2: "info",
|
|
782
|
+
1: "info"
|
|
783
|
+
}[p.priority] : "info"
|
|
784
|
+
});
|
|
785
|
+
const config = await getNtfyConfig();
|
|
786
|
+
let ntfySent = false;
|
|
787
|
+
if (config) {
|
|
788
|
+
await sendNtfyNotification(config, {
|
|
789
|
+
message: p.message,
|
|
790
|
+
title: p.title,
|
|
791
|
+
priority: p.priority,
|
|
792
|
+
tags: p.tags,
|
|
793
|
+
click: p.click,
|
|
794
|
+
markdown: p.markdown ?? true,
|
|
795
|
+
delay: p.delay,
|
|
796
|
+
actions: p.actions,
|
|
797
|
+
attach: p.attach,
|
|
798
|
+
filename: p.filename,
|
|
799
|
+
email: p.email
|
|
800
|
+
});
|
|
801
|
+
ntfySent = true;
|
|
802
|
+
}
|
|
803
|
+
const channels = ["in-app", ...ntfySent ? ["ntfy"] : []].join(", ");
|
|
804
|
+
return {
|
|
805
|
+
content: [{
|
|
806
|
+
type: "text",
|
|
807
|
+
text: `Notification sent (${channels}): ${p.title ?? p.message}`
|
|
808
|
+
}],
|
|
809
|
+
details: {
|
|
810
|
+
ok: true,
|
|
811
|
+
channels
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
const terminalTool = {
|
|
817
|
+
name: "terminal",
|
|
818
|
+
label: "Terminal",
|
|
819
|
+
description: [
|
|
820
|
+
"Manage persistent terminal/PTY sessions.",
|
|
821
|
+
"Actions: list (show sessions), create (spawn new session),",
|
|
822
|
+
"exec (send command and capture output), write (send raw input),",
|
|
823
|
+
"read (capture recent output), kill (destroy session), resize (change dimensions).",
|
|
824
|
+
"Use exec for running commands and getting results.",
|
|
825
|
+
"Use write for interactive/raw input (arrow keys, ctrl sequences, escape codes).",
|
|
826
|
+
"write supports optional wait param (ms) to capture output after sending input.",
|
|
827
|
+
"For escape key use \\x1b, for enter use \\n, for ctrl+c use \\x03."
|
|
828
|
+
].join(" "),
|
|
829
|
+
promptSnippet: "terminal: Manage persistent terminal sessions — create, exec commands, read output, kill",
|
|
830
|
+
promptGuidelines: [
|
|
831
|
+
"Use terminal exec to run commands in persistent shell sessions when you need statefulness (virtualenvs, long-running processes, interactive workflows).",
|
|
832
|
+
"For simple one-off commands, prefer bash. Use terminal for multi-step workflows needing a persistent shell.",
|
|
833
|
+
"Always list sessions before creating new ones to reuse existing sessions.",
|
|
834
|
+
"Remember session IDs for reuse across tool calls — sessions persist until killed or timed out (30min)."
|
|
835
|
+
],
|
|
836
|
+
parameters: Type.Object({
|
|
837
|
+
action: Type.Union([
|
|
838
|
+
Type.Literal("list"),
|
|
839
|
+
Type.Literal("create"),
|
|
840
|
+
Type.Literal("exec"),
|
|
841
|
+
Type.Literal("write"),
|
|
842
|
+
Type.Literal("read"),
|
|
843
|
+
Type.Literal("kill"),
|
|
844
|
+
Type.Literal("resize")
|
|
845
|
+
], { description: "Action to perform on terminal sessions" }),
|
|
846
|
+
id: Type.Optional(Type.String({ description: "Session ID (required for exec/write/read/kill/resize)" })),
|
|
847
|
+
input: Type.Optional(Type.String({ description: "Command or input text (exec appends newline automatically)" })),
|
|
848
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for new session (create)" })),
|
|
849
|
+
label: Type.Optional(Type.String({ description: "Session label (create)" })),
|
|
850
|
+
sandbox: Type.Optional(Type.Boolean({ description: "Use sandboxed virtual TTY (create, default: false)" })),
|
|
851
|
+
cols: Type.Optional(Type.Number({ description: "Terminal columns (create/resize, default: 120)" })),
|
|
852
|
+
rows: Type.Optional(Type.Number({ description: "Terminal rows (create/resize, default: 40)" })),
|
|
853
|
+
timeout: Type.Optional(Type.Number({ description: "Max ms to wait for output (exec only, default: 10000, max: 30000)" }))
|
|
854
|
+
}),
|
|
855
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
856
|
+
const p = params;
|
|
857
|
+
switch (p.action) {
|
|
858
|
+
case "list": return handleList();
|
|
859
|
+
case "create": return handleCreate(p);
|
|
860
|
+
case "exec": return handleExec(p);
|
|
861
|
+
case "write": return handleWrite(p);
|
|
862
|
+
case "read": return handleRead(p);
|
|
863
|
+
case "kill": return handleKill(p);
|
|
864
|
+
case "resize": return handleResize(p);
|
|
865
|
+
default: return err$1(`Unknown action: ${p.action}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
function handleList() {
|
|
870
|
+
const sessions = terminalManager.list();
|
|
871
|
+
if (sessions.length === 0) return ok$1("No active terminal sessions.");
|
|
872
|
+
const lines = sessions.map((s) => `- ${s.id} [${s.label}] ${s.cols}x${s.rows} cwd=${s.cwd}`);
|
|
873
|
+
return ok$1(`Active sessions (${sessions.length}):\n${lines.join("\n")}`, { sessions });
|
|
874
|
+
}
|
|
875
|
+
async function handleCreate(p) {
|
|
876
|
+
try {
|
|
877
|
+
const session = await terminalManager.create({
|
|
878
|
+
cols: p.cols ?? 120,
|
|
879
|
+
rows: p.rows ?? 40,
|
|
880
|
+
cwd: p.cwd,
|
|
881
|
+
label: p.label,
|
|
882
|
+
sandbox: p.sandbox
|
|
883
|
+
});
|
|
884
|
+
return ok$1(`Terminal session created: ${session.id} [${session.label}] ${session.cols}x${session.rows}`, {
|
|
885
|
+
id: session.id,
|
|
886
|
+
label: session.label
|
|
887
|
+
});
|
|
888
|
+
} catch (e) {
|
|
889
|
+
return err$1(`Failed to create session: ${e instanceof Error ? e.message : String(e)}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function handleExec(p) {
|
|
893
|
+
if (!p.id) return err$1("id is required for exec");
|
|
894
|
+
if (!p.input) return err$1("input is required for exec");
|
|
895
|
+
if (!terminalManager.get(p.id)) return err$1("Session not found");
|
|
896
|
+
const timeoutMs = Math.min(p.timeout ?? 1e4, 3e4);
|
|
897
|
+
const quietMs = 300;
|
|
898
|
+
const chunks = [];
|
|
899
|
+
let settled = false;
|
|
900
|
+
let quietTimer;
|
|
901
|
+
return new Promise((resolve) => {
|
|
902
|
+
const unsub = terminalManager.subscribe(p.id, (data) => {
|
|
903
|
+
chunks.push(data);
|
|
904
|
+
if (quietTimer) clearTimeout(quietTimer);
|
|
905
|
+
quietTimer = setTimeout(() => finish(false), quietMs);
|
|
906
|
+
});
|
|
907
|
+
const overallTimer = setTimeout(() => finish(true), timeoutMs);
|
|
908
|
+
function finish(timedOut) {
|
|
909
|
+
if (settled) return;
|
|
910
|
+
settled = true;
|
|
911
|
+
if (quietTimer) clearTimeout(quietTimer);
|
|
912
|
+
clearTimeout(overallTimer);
|
|
913
|
+
unsub();
|
|
914
|
+
resolve(ok$1((stripAnsi(chunks.join("")).trim() || "(no output)") + (timedOut ? "\n(timed out — output may be partial)" : ""), {
|
|
915
|
+
sessionId: p.id,
|
|
916
|
+
timedOut
|
|
917
|
+
}));
|
|
918
|
+
}
|
|
919
|
+
terminalManager.write(p.id, interpretEscapes(p.input) + "\n");
|
|
920
|
+
quietTimer = setTimeout(() => finish(false), quietMs);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
async function handleWrite(p) {
|
|
924
|
+
if (!p.id) return err$1("id is required for write");
|
|
925
|
+
if (p.input == null) return err$1("input is required for write");
|
|
926
|
+
if (!terminalManager.get(p.id)) return err$1("Session not found");
|
|
927
|
+
terminalManager.write(p.id, interpretEscapes(p.input));
|
|
928
|
+
if (p.wait && p.wait > 0) {
|
|
929
|
+
const waitMs = Math.min(p.wait, 5e3);
|
|
930
|
+
const chunks = [];
|
|
931
|
+
const unsub = terminalManager.subscribe(p.id, (data) => chunks.push(data));
|
|
932
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
933
|
+
unsub();
|
|
934
|
+
return ok$1(stripAnsi(chunks.join("")).trim() || `Input sent to ${p.id} (no output after ${waitMs}ms)`, { sessionId: p.id });
|
|
935
|
+
}
|
|
936
|
+
return ok$1(`Input sent to ${p.id}`);
|
|
937
|
+
}
|
|
938
|
+
async function handleRead(p) {
|
|
939
|
+
if (!p.id) return err$1("id is required for read");
|
|
940
|
+
if (!terminalManager.get(p.id)) return err$1("Session not found");
|
|
941
|
+
const chunks = [];
|
|
942
|
+
const unsub = terminalManager.subscribe(p.id, (data) => chunks.push(data));
|
|
943
|
+
terminalManager.nudge(p.id);
|
|
944
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
945
|
+
unsub();
|
|
946
|
+
return ok$1(stripAnsi(chunks.join("")).trim() || "(no output)", { sessionId: p.id });
|
|
947
|
+
}
|
|
948
|
+
function handleKill(p) {
|
|
949
|
+
if (!p.id) return err$1("id is required for kill");
|
|
950
|
+
terminalManager.kill(p.id);
|
|
951
|
+
return ok$1(`Session ${p.id} killed`);
|
|
952
|
+
}
|
|
953
|
+
function handleResize(p) {
|
|
954
|
+
if (!p.id) return err$1("id is required for resize");
|
|
955
|
+
const cols = p.cols ?? 120;
|
|
956
|
+
const rows = p.rows ?? 40;
|
|
957
|
+
if (!terminalManager.get(p.id)) return err$1("Session not found");
|
|
958
|
+
terminalManager.resize(p.id, cols, rows);
|
|
959
|
+
return ok$1(`Session ${p.id} resized to ${cols}x${rows}`);
|
|
960
|
+
}
|
|
961
|
+
var stripAnsiRe$1 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
|
|
962
|
+
function interpretEscapes(text) {
|
|
963
|
+
return text.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
|
|
964
|
+
}
|
|
965
|
+
function stripAnsi(text) {
|
|
966
|
+
return text.replace(stripAnsiRe$1, "");
|
|
967
|
+
}
|
|
968
|
+
function ok$1(text, details) {
|
|
969
|
+
return {
|
|
970
|
+
content: [{
|
|
971
|
+
type: "text",
|
|
972
|
+
text
|
|
973
|
+
}],
|
|
974
|
+
details: {
|
|
975
|
+
ok: true,
|
|
976
|
+
...details
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function err$1(text) {
|
|
981
|
+
return {
|
|
982
|
+
content: [{
|
|
983
|
+
type: "text",
|
|
984
|
+
text
|
|
985
|
+
}],
|
|
986
|
+
details: {
|
|
987
|
+
ok: false,
|
|
988
|
+
isError: true
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Sandboxed coding tools — real pi SDK tools backed by an in-memory filesystem.
|
|
994
|
+
* Uses the centralized sandbox manager so the filesystem is shared with virtual PTY.
|
|
995
|
+
*/
|
|
996
|
+
function createSandboxedCodingTools(opts) {
|
|
997
|
+
const sb = sandboxManager.get(opts.sandboxId, { ...opts.realCwd && { realCwd: opts.realCwd } });
|
|
998
|
+
const { fs } = sb;
|
|
999
|
+
const read = createReadTool(sb.cwd, { operations: {
|
|
1000
|
+
readFile: async (p) => {
|
|
1001
|
+
const content = await fs.readFile(p);
|
|
1002
|
+
return Buffer.from(content);
|
|
1003
|
+
},
|
|
1004
|
+
access: async (p) => {
|
|
1005
|
+
if (!await fs.exists(p)) throw new Error(`ENOENT: no such file or directory, access '${p}'`);
|
|
1006
|
+
}
|
|
1007
|
+
} });
|
|
1008
|
+
const write = createWriteTool(sb.cwd, { operations: {
|
|
1009
|
+
writeFile: async (p, content) => {
|
|
1010
|
+
await fs.writeFile(p, content);
|
|
1011
|
+
await sb.writeBack();
|
|
1012
|
+
},
|
|
1013
|
+
mkdir: (dir) => {
|
|
1014
|
+
return fs.mkdir(dir, { recursive: true });
|
|
1015
|
+
}
|
|
1016
|
+
} });
|
|
1017
|
+
const edit = createEditTool(sb.cwd, { operations: {
|
|
1018
|
+
readFile: async (p) => {
|
|
1019
|
+
const content = await fs.readFile(p);
|
|
1020
|
+
return Buffer.from(content);
|
|
1021
|
+
},
|
|
1022
|
+
writeFile: async (p, content) => {
|
|
1023
|
+
await fs.writeFile(p, content);
|
|
1024
|
+
await sb.writeBack();
|
|
1025
|
+
},
|
|
1026
|
+
access: async (p) => {
|
|
1027
|
+
if (!await fs.exists(p)) throw new Error(`ENOENT: no such file or directory, access '${p}'`);
|
|
1028
|
+
}
|
|
1029
|
+
} });
|
|
1030
|
+
return [
|
|
1031
|
+
read,
|
|
1032
|
+
createBashTool(sb.cwd, { operations: { exec: async (command, execCwd, options) => {
|
|
1033
|
+
try {
|
|
1034
|
+
const result = await sb.exec(command, { cwd: execCwd || void 0 });
|
|
1035
|
+
if (result.stdout) options.onData(Buffer.from(result.stdout));
|
|
1036
|
+
if (result.stderr) options.onData(Buffer.from(result.stderr));
|
|
1037
|
+
return { exitCode: result.exitCode };
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
options.onData(Buffer.from(err.message + "\n"));
|
|
1040
|
+
return { exitCode: 1 };
|
|
1041
|
+
}
|
|
1042
|
+
} } }),
|
|
1043
|
+
edit,
|
|
1044
|
+
write
|
|
1045
|
+
];
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* MIT License
|
|
1049
|
+
* Copyright (c) 2024 Mario Zechner
|
|
1050
|
+
* Based on https://github.com/badlogic/pi-skills/tree/main/brave-search
|
|
1051
|
+
*/
|
|
1052
|
+
const webSearchTool = {
|
|
1053
|
+
name: "web_search",
|
|
1054
|
+
label: "Web Search",
|
|
1055
|
+
description: [
|
|
1056
|
+
"Search the web using the Brave Search API.",
|
|
1057
|
+
"Returns titles, links, snippets, and optionally full page content as markdown.",
|
|
1058
|
+
"Requires BRAVE_API_KEY environment variable."
|
|
1059
|
+
].join(" "),
|
|
1060
|
+
promptSnippet: "web_search: Search the web and optionally fetch page content as markdown",
|
|
1061
|
+
promptGuidelines: [
|
|
1062
|
+
"Use web_search to look up current information, documentation, error messages, or anything that benefits from live web results.",
|
|
1063
|
+
"Enable content=true to fetch and read full page content as markdown (slower but more detailed).",
|
|
1064
|
+
"Use freshness to filter by time: pd (past day), pw (past week), pm (past month), py (past year).",
|
|
1065
|
+
"Keep count reasonable (3-5) when fetching content to avoid slow responses."
|
|
1066
|
+
],
|
|
1067
|
+
parameters: Type.Object({
|
|
1068
|
+
query: Type.String({ description: "Search query" }),
|
|
1069
|
+
count: Type.Optional(Type.Number({ description: "Number of results (default: 5, max: 20)" })),
|
|
1070
|
+
content: Type.Optional(Type.Boolean({ description: "Fetch full page content as markdown (default: false, slower)" })),
|
|
1071
|
+
country: Type.Optional(Type.String({ description: "Country code for results (default: \"US\")" })),
|
|
1072
|
+
freshness: Type.Optional(Type.String({ description: "Filter by time: pd (past day), pw (past week), pm (past month), py (past year)" }))
|
|
1073
|
+
}),
|
|
1074
|
+
async execute(_toolCallId, params, signal) {
|
|
1075
|
+
const p = params;
|
|
1076
|
+
const apiKey = config.get("braveApiKey");
|
|
1077
|
+
if (!apiKey) return err("BRAVE_API_KEY is not configured. Set it via Settings => Config => braveApiKey. Get a key at https://api-dashboard.search.brave.com/app/keys");
|
|
1078
|
+
const count = Math.min(Math.max(p.count ?? 5, 1), 20);
|
|
1079
|
+
try {
|
|
1080
|
+
const results = await fetchBraveResults(apiKey, p.query, count, p.country, p.freshness, signal);
|
|
1081
|
+
if (results.length === 0) return ok("No results found.", {
|
|
1082
|
+
query: p.query,
|
|
1083
|
+
count: 0
|
|
1084
|
+
});
|
|
1085
|
+
if (p.content) await Promise.all(results.map(async (r) => {
|
|
1086
|
+
r.content = await fetchPageContent(r.link, signal);
|
|
1087
|
+
}));
|
|
1088
|
+
const lines = [];
|
|
1089
|
+
for (const [i, r] of results.entries()) {
|
|
1090
|
+
lines.push(`--- Result ${i + 1} ---`);
|
|
1091
|
+
lines.push(`Title: ${r.title}`);
|
|
1092
|
+
lines.push(`Link: ${r.link}`);
|
|
1093
|
+
if (r.age) lines.push(`Age: ${r.age}`);
|
|
1094
|
+
lines.push(`Snippet: ${r.snippet}`);
|
|
1095
|
+
if (r.content) lines.push(`Content:\n${r.content}`);
|
|
1096
|
+
lines.push("");
|
|
1097
|
+
}
|
|
1098
|
+
return ok(lines.join("\n"), {
|
|
1099
|
+
query: p.query,
|
|
1100
|
+
count: results.length
|
|
1101
|
+
});
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
return err(`Search failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
async function fetchBraveResults(apiKey, query, count, country, freshness, signal) {
|
|
1108
|
+
const params = new URLSearchParams({
|
|
1109
|
+
q: query,
|
|
1110
|
+
count: String(count),
|
|
1111
|
+
country: country?.toUpperCase() || "US"
|
|
1112
|
+
});
|
|
1113
|
+
if (freshness) params.append("freshness", freshness);
|
|
1114
|
+
const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
|
|
1115
|
+
headers: {
|
|
1116
|
+
Accept: "application/json",
|
|
1117
|
+
"Accept-Encoding": "gzip",
|
|
1118
|
+
"X-Subscription-Token": apiKey
|
|
1119
|
+
},
|
|
1120
|
+
signal
|
|
1121
|
+
});
|
|
1122
|
+
if (!res.ok) {
|
|
1123
|
+
const text = await res.text();
|
|
1124
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}\n${text}`);
|
|
1125
|
+
}
|
|
1126
|
+
const data = await res.json();
|
|
1127
|
+
const results = [];
|
|
1128
|
+
if (data.web?.results) for (const r of data.web.results) {
|
|
1129
|
+
if (results.length >= count) break;
|
|
1130
|
+
results.push({
|
|
1131
|
+
title: r.title || "",
|
|
1132
|
+
link: r.url || "",
|
|
1133
|
+
snippet: r.description || "",
|
|
1134
|
+
age: r.age || r.page_age || ""
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return results;
|
|
1138
|
+
}
|
|
1139
|
+
async function fetchPageContent(url, signal) {
|
|
1140
|
+
try {
|
|
1141
|
+
const res = await fetch(url, {
|
|
1142
|
+
headers: {
|
|
1143
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
1144
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
1145
|
+
},
|
|
1146
|
+
signal: signal ?? AbortSignal.timeout(1e4)
|
|
1147
|
+
});
|
|
1148
|
+
if (!res.ok) return `(HTTP ${res.status})`;
|
|
1149
|
+
return extractReadableText(await res.text(), url);
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
return `(Error: ${e instanceof Error ? e.message : String(e)})`;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
async function extractReadableText(html, url) {
|
|
1155
|
+
try {
|
|
1156
|
+
const { Readability } = await import("../_libs/_.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
|
|
1157
|
+
const { JSDOM } = await import("../_libs/_4.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
|
|
1158
|
+
const article = new Readability(new JSDOM(html, { url }).window.document).parse();
|
|
1159
|
+
if (article?.content) return (await htmlToMarkdown(article.content)).slice(0, 5e3);
|
|
1160
|
+
const doc = new JSDOM(html, { url }).window.document;
|
|
1161
|
+
doc.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach((el) => el.remove());
|
|
1162
|
+
const text = (doc.querySelector("main, article, [role='main'], .content, #content") || doc.body)?.textContent || "";
|
|
1163
|
+
if (text.trim().length > 100) return text.trim().slice(0, 5e3);
|
|
1164
|
+
return "(Could not extract content)";
|
|
1165
|
+
} catch {
|
|
1166
|
+
return stripHtmlTags$1(html).slice(0, 5e3) || "(Could not extract content)";
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
async function htmlToMarkdown(html) {
|
|
1170
|
+
try {
|
|
1171
|
+
const TurndownService = (await import("../_libs/_29.mjs")).default;
|
|
1172
|
+
const { gfm } = await import("../_libs/_93.mjs");
|
|
1173
|
+
const turndown = new TurndownService({
|
|
1174
|
+
headingStyle: "atx",
|
|
1175
|
+
codeBlockStyle: "fenced"
|
|
1176
|
+
});
|
|
1177
|
+
turndown.use(gfm);
|
|
1178
|
+
turndown.addRule("removeEmptyLinks", {
|
|
1179
|
+
filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
|
|
1180
|
+
replacement: () => ""
|
|
1181
|
+
});
|
|
1182
|
+
return turndown.turndown(html).replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "").replace(/ +/g, " ").replace(/\s+,/g, ",").replace(/\s+\./g, ".").replace(/\n{3,}/g, "\n\n").trim();
|
|
1183
|
+
} catch {
|
|
1184
|
+
return stripHtmlTags$1(html);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function stripHtmlTags$1(html) {
|
|
1188
|
+
return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
1189
|
+
}
|
|
1190
|
+
function ok(text, details) {
|
|
1191
|
+
return {
|
|
1192
|
+
content: [{
|
|
1193
|
+
type: "text",
|
|
1194
|
+
text
|
|
1195
|
+
}],
|
|
1196
|
+
details: {
|
|
1197
|
+
ok: true,
|
|
1198
|
+
...details
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function err(text) {
|
|
1203
|
+
return {
|
|
1204
|
+
content: [{
|
|
1205
|
+
type: "text",
|
|
1206
|
+
text
|
|
1207
|
+
}],
|
|
1208
|
+
details: {
|
|
1209
|
+
ok: false,
|
|
1210
|
+
isError: true
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
var logger$5 = createLogger("pi");
|
|
1215
|
+
/**
|
|
1216
|
+
* Model preference order for fallback resolution.
|
|
1217
|
+
* Entries with `model` try a specific model; without tries any available from that provider.
|
|
1218
|
+
*/
|
|
1219
|
+
var MODEL_PREFERENCE = [
|
|
1220
|
+
{
|
|
1221
|
+
provider: "openai-codex",
|
|
1222
|
+
model: "gpt-5.3-codex"
|
|
1223
|
+
},
|
|
1224
|
+
{ provider: "openai-codex" },
|
|
1225
|
+
{ provider: "anthropic" },
|
|
1226
|
+
{ provider: "google" }
|
|
1227
|
+
];
|
|
1228
|
+
/** Provider search order derived from MODEL_PREFERENCE */
|
|
1229
|
+
var PROVIDER_ORDER = [...new Set(MODEL_PREFERENCE.map((e) => e.provider))];
|
|
1230
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1231
|
+
const authStorage = AuthStorage.create();
|
|
1232
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
1233
|
+
async function getOrCreateSession(group, input) {
|
|
1234
|
+
const sessionDir = path.join(SESSIONS_DIR, group.folder);
|
|
1235
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
1236
|
+
let managed = sessions.get(input.chatJid);
|
|
1237
|
+
if (!managed) {
|
|
1238
|
+
streamBus.emit({
|
|
1239
|
+
type: "session_init",
|
|
1240
|
+
chatJid: input.chatJid
|
|
1241
|
+
});
|
|
1242
|
+
managed = await createManagedSession(group, input, sessionDir);
|
|
1243
|
+
sessions.set(input.chatJid, managed);
|
|
1244
|
+
}
|
|
1245
|
+
managed.lastActivity = Date.now();
|
|
1246
|
+
return managed;
|
|
1247
|
+
}
|
|
1248
|
+
function resolveModel() {
|
|
1249
|
+
const available = getFilteredModels();
|
|
1250
|
+
const piProvider = config.get("piProvider");
|
|
1251
|
+
const piModel = config.get("piModel");
|
|
1252
|
+
if (piProvider && piModel) {
|
|
1253
|
+
const model = findAvailable(available, piProvider, piModel);
|
|
1254
|
+
if (model) return model;
|
|
1255
|
+
}
|
|
1256
|
+
if (piModel) {
|
|
1257
|
+
const slashIdx = piModel.indexOf("/");
|
|
1258
|
+
if (slashIdx > 0) {
|
|
1259
|
+
const model = findAvailable(available, piModel.slice(0, slashIdx), piModel.slice(slashIdx + 1));
|
|
1260
|
+
if (model) return model;
|
|
1261
|
+
}
|
|
1262
|
+
for (const provider of PROVIDER_ORDER) {
|
|
1263
|
+
const model = findAvailable(available, provider, piModel);
|
|
1264
|
+
if (model) return model;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return resolveDefaultModel();
|
|
1268
|
+
}
|
|
1269
|
+
function resolveGroupModel(group) {
|
|
1270
|
+
if (!group.model) {
|
|
1271
|
+
const model = resolveModel();
|
|
1272
|
+
logger$5.debug({
|
|
1273
|
+
group: group.name,
|
|
1274
|
+
resolved: fmtModel(model),
|
|
1275
|
+
source: "global"
|
|
1276
|
+
}, "Model resolved");
|
|
1277
|
+
return model;
|
|
1278
|
+
}
|
|
1279
|
+
const available = getFilteredModels();
|
|
1280
|
+
const modelStr = group.model;
|
|
1281
|
+
const slashIdx = modelStr.indexOf("/");
|
|
1282
|
+
if (slashIdx > 0) {
|
|
1283
|
+
const model = findAvailable(available, modelStr.slice(0, slashIdx), modelStr.slice(slashIdx + 1));
|
|
1284
|
+
if (model) {
|
|
1285
|
+
logger$5.debug({
|
|
1286
|
+
group: group.name,
|
|
1287
|
+
resolved: fmtModel(model),
|
|
1288
|
+
source: "group"
|
|
1289
|
+
}, "Model resolved");
|
|
1290
|
+
return model;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
for (const provider of PROVIDER_ORDER) {
|
|
1294
|
+
const model = findAvailable(available, provider, modelStr);
|
|
1295
|
+
if (model) {
|
|
1296
|
+
logger$5.debug({
|
|
1297
|
+
group: group.name,
|
|
1298
|
+
resolved: fmtModel(model),
|
|
1299
|
+
source: "group-search"
|
|
1300
|
+
}, "Model resolved");
|
|
1301
|
+
return model;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
logger$5.warn({
|
|
1305
|
+
group: group.name,
|
|
1306
|
+
wanted: modelStr,
|
|
1307
|
+
available: available.map(fmtModel)
|
|
1308
|
+
}, "Group model not found in available models, falling back to global");
|
|
1309
|
+
const fallback = resolveModel();
|
|
1310
|
+
logger$5.debug({
|
|
1311
|
+
group: group.name,
|
|
1312
|
+
resolved: fmtModel(fallback),
|
|
1313
|
+
source: "fallback"
|
|
1314
|
+
}, "Model resolved");
|
|
1315
|
+
return fallback;
|
|
1316
|
+
}
|
|
1317
|
+
function resolveDefaultModel() {
|
|
1318
|
+
const available = getFilteredModels();
|
|
1319
|
+
for (const pref of MODEL_PREFERENCE) if (pref.model) {
|
|
1320
|
+
const model = findAvailable(available, pref.provider, pref.model);
|
|
1321
|
+
if (model) return model;
|
|
1322
|
+
} else {
|
|
1323
|
+
const model = available.find((m) => m.provider === pref.provider);
|
|
1324
|
+
if (model) return model;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async function createManagedSession(group, input, sessionDir) {
|
|
1328
|
+
const groupDir = path.join(GROUPS_DIR, group.folder);
|
|
1329
|
+
const sandboxEnforced = config.get("sandbox") === "enforced";
|
|
1330
|
+
const sandbox = sandboxEnforced || !!group.sandbox;
|
|
1331
|
+
logger$5.info({
|
|
1332
|
+
group: group.name,
|
|
1333
|
+
folder: group.folder,
|
|
1334
|
+
isMain: input.isMain,
|
|
1335
|
+
hasSession: !!input.sessionId,
|
|
1336
|
+
sandbox,
|
|
1337
|
+
...sandboxEnforced && { sandboxSource: "enforced" }
|
|
1338
|
+
}, "Creating pi SDK session");
|
|
1339
|
+
const model = resolveGroupModel(group);
|
|
1340
|
+
const settingsManager = SettingsManager.inMemory({
|
|
1341
|
+
compaction: { enabled: true },
|
|
1342
|
+
retry: {
|
|
1343
|
+
enabled: true,
|
|
1344
|
+
maxRetries: 3
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
const systemPrompt = await buildSystemPrompt({
|
|
1348
|
+
assistantName: input.assistantName,
|
|
1349
|
+
dir: groupDir,
|
|
1350
|
+
modelName: `${model?.provider}/${model?.id}`,
|
|
1351
|
+
soul: group.soul
|
|
1352
|
+
});
|
|
1353
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
1354
|
+
cwd: groupDir,
|
|
1355
|
+
settingsManager,
|
|
1356
|
+
systemPromptOverride: () => systemPrompt,
|
|
1357
|
+
...sandbox && {
|
|
1358
|
+
agentDir: groupDir,
|
|
1359
|
+
noExtensions: true,
|
|
1360
|
+
noSkills: true,
|
|
1361
|
+
noPromptTemplates: true,
|
|
1362
|
+
noThemes: true,
|
|
1363
|
+
agentsFilesOverride: (_base) => ({ agentsFiles: [] })
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
await resourceLoader.reload();
|
|
1367
|
+
const sessionManager = input.sessionId ? SessionManager.continueRecent(groupDir, sessionDir) : SessionManager.create(groupDir, sessionDir);
|
|
1368
|
+
const thinkingLevel = group.thinkingLevel || "off";
|
|
1369
|
+
if (sandbox) logger$5.info({ group: group.name }, "Using sandboxed coding tools (in-memory filesystem)");
|
|
1370
|
+
const sandboxedTools = sandbox ? Object.fromEntries(createSandboxedCodingTools({
|
|
1371
|
+
sandboxId: `~${group.folder}`,
|
|
1372
|
+
realCwd: groupDir
|
|
1373
|
+
}).map((t) => [t.name, t])) : void 0;
|
|
1374
|
+
const { session, modelFallbackMessage } = await createAgentSession({
|
|
1375
|
+
cwd: groupDir,
|
|
1376
|
+
model,
|
|
1377
|
+
thinkingLevel,
|
|
1378
|
+
authStorage,
|
|
1379
|
+
modelRegistry,
|
|
1380
|
+
customTools: sandbox ? [notifyTool, webSearchTool] : [
|
|
1381
|
+
browserTool,
|
|
1382
|
+
createMessageTool(group.folder),
|
|
1383
|
+
notesTool,
|
|
1384
|
+
notifyTool,
|
|
1385
|
+
terminalTool,
|
|
1386
|
+
webSearchTool
|
|
1387
|
+
],
|
|
1388
|
+
resourceLoader,
|
|
1389
|
+
sessionManager,
|
|
1390
|
+
settingsManager
|
|
1391
|
+
});
|
|
1392
|
+
if (sandboxedTools) {
|
|
1393
|
+
session._baseToolsOverride = sandboxedTools;
|
|
1394
|
+
await session.reload();
|
|
1395
|
+
}
|
|
1396
|
+
if (modelFallbackMessage) logger$5.warn({
|
|
1397
|
+
group: group.name,
|
|
1398
|
+
message: modelFallbackMessage
|
|
1399
|
+
}, "Model fallback");
|
|
1400
|
+
const actualModel = session.model;
|
|
1401
|
+
if (model && actualModel && (actualModel.provider !== model.provider || actualModel.id !== model.id)) {
|
|
1402
|
+
logger$5.warn({
|
|
1403
|
+
group: group.name,
|
|
1404
|
+
requested: fmtModel(model),
|
|
1405
|
+
actual: fmtModel(actualModel)
|
|
1406
|
+
}, "SDK used different model than requested — forcing requested model");
|
|
1407
|
+
await session.setModel(model);
|
|
1408
|
+
}
|
|
1409
|
+
logger$5.info({
|
|
1410
|
+
group: group.name,
|
|
1411
|
+
sessionId: session.sessionId,
|
|
1412
|
+
model: fmtModel(session.model),
|
|
1413
|
+
groupModel: group.model || "default"
|
|
1414
|
+
}, "Pi SDK session created");
|
|
1415
|
+
return {
|
|
1416
|
+
session,
|
|
1417
|
+
groupFolder: group.folder,
|
|
1418
|
+
sessionDir,
|
|
1419
|
+
lastActivity: Date.now()
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
/** Date suffix pattern (e.g. `-20241022`, `-20250514`) */
|
|
1423
|
+
var DATE_SUFFIX_RE = /-\d{8}$/;
|
|
1424
|
+
/**
|
|
1425
|
+
* Returns available models filtered and sorted:
|
|
1426
|
+
* - Removes models with date suffixes in their ID (prefer canonical short names)
|
|
1427
|
+
* - Removes all haiku models (too weak for coding tasks)
|
|
1428
|
+
* - Sorts by MODEL_PREFERENCE provider order (preferred providers first, unknown last)
|
|
1429
|
+
*/
|
|
1430
|
+
function getFilteredModels() {
|
|
1431
|
+
return modelRegistry.getAvailable().filter((m) => {
|
|
1432
|
+
if (DATE_SUFFIX_RE.test(m.id)) return false;
|
|
1433
|
+
if (/haiku/i.test(m.id)) return false;
|
|
1434
|
+
return true;
|
|
1435
|
+
}).sort((a, b) => {
|
|
1436
|
+
const ai = PROVIDER_ORDER.indexOf(a.provider);
|
|
1437
|
+
const bi = PROVIDER_ORDER.indexOf(b.provider);
|
|
1438
|
+
return (ai === -1 ? PROVIDER_ORDER.length : ai) - (bi === -1 ? PROVIDER_ORDER.length : bi);
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
function findAvailable(available, provider, modelId) {
|
|
1442
|
+
return available.find((m) => m.provider === provider && m.id === modelId);
|
|
1443
|
+
}
|
|
1444
|
+
function fmtModel(m) {
|
|
1445
|
+
return m ? `${m.provider}/${m.id}` : "none";
|
|
1446
|
+
}
|
|
1447
|
+
function getPiSdkStatus$1(options) {
|
|
1448
|
+
const { sessions, modelRegistry, authStorage, resolvedModel, configuredProvider, configuredModel } = options;
|
|
1449
|
+
const now = Date.now();
|
|
1450
|
+
const allModels = modelRegistry.getAll();
|
|
1451
|
+
const availableModels = modelRegistry.getAvailable();
|
|
1452
|
+
const availableModelKeys = new Set(availableModels.map((model) => getModelKey(model.provider, model.id)));
|
|
1453
|
+
const sessionList = Array.from(sessions.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([chatJid, managed]) => {
|
|
1454
|
+
const contextUsage = managed.session.getContextUsage();
|
|
1455
|
+
return {
|
|
1456
|
+
chatJid,
|
|
1457
|
+
groupFolder: managed.groupFolder,
|
|
1458
|
+
sessionDir: managed.sessionDir,
|
|
1459
|
+
lastActivity: managed.lastActivity,
|
|
1460
|
+
idleMs: Math.max(0, now - managed.lastActivity),
|
|
1461
|
+
session: {
|
|
1462
|
+
id: managed.session.sessionId,
|
|
1463
|
+
file: managed.session.sessionFile ?? null,
|
|
1464
|
+
name: managed.session.sessionName ?? null,
|
|
1465
|
+
messageCount: managed.session.messages.length,
|
|
1466
|
+
model: managed.session.model ? serializeModel(managed.session.model) : null,
|
|
1467
|
+
thinkingLevel: managed.session.thinkingLevel,
|
|
1468
|
+
contextUsage: contextUsage ? {
|
|
1469
|
+
tokens: contextUsage.tokens,
|
|
1470
|
+
contextWindow: contextUsage.contextWindow,
|
|
1471
|
+
percent: contextUsage.percent
|
|
1472
|
+
} : null
|
|
1473
|
+
},
|
|
1474
|
+
runtime: {
|
|
1475
|
+
isStreaming: managed.session.isStreaming,
|
|
1476
|
+
isCompacting: managed.session.isCompacting,
|
|
1477
|
+
isRetrying: managed.session.isRetrying,
|
|
1478
|
+
retryAttempt: managed.session.retryAttempt,
|
|
1479
|
+
autoRetryEnabled: managed.session.autoRetryEnabled,
|
|
1480
|
+
autoCompactionEnabled: managed.session.autoCompactionEnabled,
|
|
1481
|
+
pendingMessageCount: managed.session.pendingMessageCount,
|
|
1482
|
+
isBashRunning: managed.session.isBashRunning,
|
|
1483
|
+
hasPendingBashMessages: managed.session.hasPendingBashMessages,
|
|
1484
|
+
steeringMode: managed.session.steeringMode,
|
|
1485
|
+
followUpMode: managed.session.followUpMode
|
|
1486
|
+
},
|
|
1487
|
+
settings: {
|
|
1488
|
+
retry: managed.session.settingsManager.getRetrySettings(),
|
|
1489
|
+
compaction: managed.session.settingsManager.getCompactionSettings()
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
});
|
|
1493
|
+
const authProviders = new Set([...allModels.map((model) => model.provider), ...authStorage.list()]);
|
|
1494
|
+
return {
|
|
1495
|
+
sdk: {
|
|
1496
|
+
activeSessions: sessions.size,
|
|
1497
|
+
streamingSessions: sessionList.filter((session) => session.runtime.isStreaming).length,
|
|
1498
|
+
compactingSessions: sessionList.filter((session) => session.runtime.isCompacting).length,
|
|
1499
|
+
retryingSessions: sessionList.filter((session) => session.runtime.isRetrying).length,
|
|
1500
|
+
bashRunningSessions: sessionList.filter((session) => session.runtime.isBashRunning).length,
|
|
1501
|
+
timestamp: now
|
|
1502
|
+
},
|
|
1503
|
+
model: {
|
|
1504
|
+
configured: {
|
|
1505
|
+
provider: configuredProvider ?? null,
|
|
1506
|
+
model: configuredModel ?? null
|
|
1507
|
+
},
|
|
1508
|
+
resolved: resolvedModel ? {
|
|
1509
|
+
...serializeModel(resolvedModel),
|
|
1510
|
+
isAvailable: availableModelKeys.has(getModelKey(resolvedModel.provider, resolvedModel.id)),
|
|
1511
|
+
usesOAuth: modelRegistry.isUsingOAuth(resolvedModel)
|
|
1512
|
+
} : null,
|
|
1513
|
+
registry: {
|
|
1514
|
+
totalModels: allModels.length,
|
|
1515
|
+
availableModels: availableModels.length,
|
|
1516
|
+
loadError: modelRegistry.getError() ?? null
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
auth: {
|
|
1520
|
+
configuredProviders: authStorage.list().sort(),
|
|
1521
|
+
oauthProviders: authStorage.getOAuthProviders().map((provider) => ({
|
|
1522
|
+
id: provider.id,
|
|
1523
|
+
name: provider.name
|
|
1524
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
1525
|
+
providers: Array.from(authProviders).sort().map((provider) => ({
|
|
1526
|
+
provider,
|
|
1527
|
+
hasAuth: authStorage.hasAuth(provider)
|
|
1528
|
+
}))
|
|
1529
|
+
},
|
|
1530
|
+
sessions: sessionList
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function serializeModel(model) {
|
|
1534
|
+
return {
|
|
1535
|
+
provider: model.provider,
|
|
1536
|
+
id: model.id,
|
|
1537
|
+
name: model.name,
|
|
1538
|
+
api: model.api,
|
|
1539
|
+
reasoning: model.reasoning ?? false,
|
|
1540
|
+
input: model.input ?? ["text"],
|
|
1541
|
+
contextWindow: model.contextWindow ?? null,
|
|
1542
|
+
maxTokens: model.maxTokens ?? null,
|
|
1543
|
+
cost: model.cost ?? null
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
function getModelKey(provider, modelId) {
|
|
1547
|
+
return `${provider}/${modelId}`;
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Pi agent — runs pi coding agent in-process via the SDK.
|
|
1551
|
+
*
|
|
1552
|
+
* Uses `createAgentSession` from @mariozechner/pi-coding-agent directly.
|
|
1553
|
+
* Sessions are persisted per-group and automatically continued across restarts.
|
|
1554
|
+
*/
|
|
1555
|
+
var pi_exports = /* @__PURE__ */ __exportAll({
|
|
1556
|
+
getFilteredModels: () => getFilteredModels,
|
|
1557
|
+
getPiSdkStatus: () => getPiSdkStatus,
|
|
1558
|
+
kill: () => kill,
|
|
1559
|
+
modelRegistry: () => modelRegistry,
|
|
1560
|
+
run: () => run,
|
|
1561
|
+
sendMessage: () => sendMessage,
|
|
1562
|
+
shutdown: () => shutdown,
|
|
1563
|
+
warmUp: () => warmUp
|
|
1564
|
+
});
|
|
1565
|
+
var logger$4 = createLogger("pi");
|
|
1566
|
+
async function run(group, input, onOutput) {
|
|
1567
|
+
return sendPrompt(await getOrCreateSession(group, input), group, input, onOutput);
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Deliver a message to an active streaming session.
|
|
1571
|
+
* If the message ends with `!`, uses `steer` (interrupts agent after current tool).
|
|
1572
|
+
* Otherwise uses `followUp` (delivered after agent finishes current work).
|
|
1573
|
+
* Returns the delivery method, or `false` if no active streaming session.
|
|
1574
|
+
*/
|
|
1575
|
+
function sendMessage(groupJid, message) {
|
|
1576
|
+
const managed = sessions.get(groupJid);
|
|
1577
|
+
if (!managed || !managed.session.isStreaming) return false;
|
|
1578
|
+
const { session } = managed;
|
|
1579
|
+
if (message.includes("!")) {
|
|
1580
|
+
session.steer(message).catch((err) => {
|
|
1581
|
+
logger$4.error({
|
|
1582
|
+
group: managed.groupFolder,
|
|
1583
|
+
err
|
|
1584
|
+
}, "Failed to steer message");
|
|
1585
|
+
});
|
|
1586
|
+
return "steer";
|
|
1587
|
+
} else {
|
|
1588
|
+
session.followUp(message).catch((err) => {
|
|
1589
|
+
logger$4.error({
|
|
1590
|
+
group: managed.groupFolder,
|
|
1591
|
+
err
|
|
1592
|
+
}, "Failed to queue follow-up");
|
|
1593
|
+
});
|
|
1594
|
+
return "followup";
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
async function kill(groupJid) {
|
|
1598
|
+
const managed = sessions.get(groupJid);
|
|
1599
|
+
if (managed) {
|
|
1600
|
+
managed.aborted = true;
|
|
1601
|
+
try {
|
|
1602
|
+
await managed.session.abort();
|
|
1603
|
+
} catch {}
|
|
1604
|
+
if (managed.abortDone) await managed.abortDone;
|
|
1605
|
+
managed.session.dispose();
|
|
1606
|
+
sessions.delete(groupJid);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Eagerly restore in-memory sessions for groups that had persisted sessions.
|
|
1611
|
+
* Called on server boot so sessions are warm before message recovery.
|
|
1612
|
+
*/
|
|
1613
|
+
async function warmUp(groups, storedSessions, assistantName) {
|
|
1614
|
+
const folderToJid = /* @__PURE__ */ new Map();
|
|
1615
|
+
for (const [jid, group] of Object.entries(groups)) folderToJid.set(group.folder, {
|
|
1616
|
+
jid,
|
|
1617
|
+
group
|
|
1618
|
+
});
|
|
1619
|
+
const toRestore = Object.entries(storedSessions).filter(([folder]) => folderToJid.has(folder) && !sessions.has(folderToJid.get(folder).jid));
|
|
1620
|
+
if (toRestore.length === 0) return;
|
|
1621
|
+
logger$4.info({ count: toRestore.length }, "Warming up persisted sessions");
|
|
1622
|
+
const results = await Promise.allSettled(toRestore.map(async ([folder, sessionId]) => {
|
|
1623
|
+
const { jid, group } = folderToJid.get(folder);
|
|
1624
|
+
await getOrCreateSession(group, {
|
|
1625
|
+
prompt: "",
|
|
1626
|
+
sessionId,
|
|
1627
|
+
groupFolder: folder,
|
|
1628
|
+
chatJid: jid,
|
|
1629
|
+
isMain: folder === "main",
|
|
1630
|
+
assistantName
|
|
1631
|
+
});
|
|
1632
|
+
return {
|
|
1633
|
+
jid,
|
|
1634
|
+
group: group.name
|
|
1635
|
+
};
|
|
1636
|
+
}));
|
|
1637
|
+
for (const r of results) if (r.status === "fulfilled") logger$4.info({ group: r.value.group }, "Session restored");
|
|
1638
|
+
else logger$4.warn({ err: r.reason }, "Failed to restore session");
|
|
1639
|
+
}
|
|
1640
|
+
async function shutdown(timeoutMs) {
|
|
1641
|
+
for (const [jid, managed] of sessions) {
|
|
1642
|
+
try {
|
|
1643
|
+
managed.session.dispose();
|
|
1644
|
+
} catch {}
|
|
1645
|
+
sessions.delete(jid);
|
|
1646
|
+
}
|
|
1647
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(timeoutMs, 1e3)));
|
|
1648
|
+
}
|
|
1649
|
+
function getPiSdkStatus() {
|
|
1650
|
+
return getPiSdkStatus$1({
|
|
1651
|
+
sessions,
|
|
1652
|
+
modelRegistry,
|
|
1653
|
+
authStorage,
|
|
1654
|
+
resolvedModel: resolveModel(),
|
|
1655
|
+
configuredProvider: config.get("piProvider"),
|
|
1656
|
+
configuredModel: config.get("piModel")
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
var commands = {
|
|
1660
|
+
help: handleHelp,
|
|
1661
|
+
info: handleInfo,
|
|
1662
|
+
new: handleNew,
|
|
1663
|
+
stop: handleStop,
|
|
1664
|
+
model: handleModel,
|
|
1665
|
+
thinking: handleThinking,
|
|
1666
|
+
compact: handleCompact
|
|
1667
|
+
};
|
|
1668
|
+
var VALID_THINKING_LEVELS = [
|
|
1669
|
+
"off",
|
|
1670
|
+
"minimal",
|
|
1671
|
+
"low",
|
|
1672
|
+
"medium",
|
|
1673
|
+
"high",
|
|
1674
|
+
"xhigh"
|
|
1675
|
+
];
|
|
1676
|
+
/** Descriptions for registering commands with external channels (e.g. Telegram). */
|
|
1677
|
+
const commandDescriptions = [
|
|
1678
|
+
{
|
|
1679
|
+
command: "help",
|
|
1680
|
+
description: "Show available commands"
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
command: "info",
|
|
1684
|
+
description: "Show session info and workspace overview"
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
command: "new",
|
|
1688
|
+
description: "Reset session and start fresh"
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
command: "stop",
|
|
1692
|
+
description: "Stop the current agent session"
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
command: "model",
|
|
1696
|
+
description: "List or set model",
|
|
1697
|
+
hasParams: true
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
command: "thinking",
|
|
1701
|
+
description: "Show or set thinking level",
|
|
1702
|
+
hasParams: true,
|
|
1703
|
+
paramOptions: VALID_THINKING_LEVELS
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
command: "compact",
|
|
1707
|
+
description: "Compact current session context"
|
|
1708
|
+
}
|
|
1709
|
+
];
|
|
1710
|
+
/** Commands that bypass the queue and execute immediately (even during streaming). */
|
|
1711
|
+
var priorityCommands = {
|
|
1712
|
+
stop: handleStop,
|
|
1713
|
+
new: handleNew,
|
|
1714
|
+
info: handleInfo
|
|
1715
|
+
};
|
|
1716
|
+
/**
|
|
1717
|
+
* Try to handle a priority command that must bypass the queue.
|
|
1718
|
+
* Called from the message loop and send API before enqueueing.
|
|
1719
|
+
* Returns true if the message was a priority command (consumed).
|
|
1720
|
+
*/
|
|
1721
|
+
function tryPriorityCommand(ctx, text) {
|
|
1722
|
+
if (!text.startsWith("/")) return false;
|
|
1723
|
+
const spaceIdx = text.indexOf(" ");
|
|
1724
|
+
const name = (spaceIdx > 0 ? text.slice(1, spaceIdx) : text.slice(1)).toLowerCase();
|
|
1725
|
+
const handler = priorityCommands[name];
|
|
1726
|
+
if (!handler) return false;
|
|
1727
|
+
Promise.resolve(handler(ctx, "")).catch((err) => {
|
|
1728
|
+
ctx.server.sendBotMessage(ctx.chatJid, `Command /${name} failed: ${String(err)}`).catch(() => {});
|
|
1729
|
+
});
|
|
1730
|
+
return true;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Try to handle a message as a `/command`. Returns true if handled.
|
|
1734
|
+
*/
|
|
1735
|
+
async function tryCommand(ctx, text) {
|
|
1736
|
+
if (!text.startsWith("/")) return false;
|
|
1737
|
+
const spaceIdx = text.indexOf(" ");
|
|
1738
|
+
const name = (spaceIdx > 0 ? text.slice(1, spaceIdx) : text.slice(1)).toLowerCase();
|
|
1739
|
+
const arg = spaceIdx > 0 ? text.slice(spaceIdx + 1).trim() : "";
|
|
1740
|
+
const handler = commands[name];
|
|
1741
|
+
if (handler) {
|
|
1742
|
+
try {
|
|
1743
|
+
await handler(ctx, arg);
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
await ctx.server.sendBotMessage(ctx.chatJid, `Command /${name} failed: ${String(err)}`);
|
|
1746
|
+
}
|
|
1747
|
+
return true;
|
|
1748
|
+
}
|
|
1749
|
+
await ctx.server.sendBotMessage(ctx.chatJid, `Unknown command \`/${name}\`. Use \`/help\` for available commands.`);
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
async function handleHelp(ctx) {
|
|
1753
|
+
const lines = ["**Available commands:**", ...commandDescriptions.map((c) => `- \`/${c.command}\` — ${c.description}`)];
|
|
1754
|
+
await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
|
|
1755
|
+
}
|
|
1756
|
+
async function handleInfo(ctx) {
|
|
1757
|
+
const resolved = resolveGroupModel(ctx.group);
|
|
1758
|
+
const modelLabel = resolved ? `${resolved.provider}/${resolved.id}` : "none";
|
|
1759
|
+
const modelSource = ctx.group.model ? `group (${ctx.group.model})` : "default";
|
|
1760
|
+
const managed = sessions.get(ctx.chatJid);
|
|
1761
|
+
const lines = [
|
|
1762
|
+
`**Group:** ${ctx.group.name} (\`${ctx.group.folder}\`)`,
|
|
1763
|
+
`**Model:** \`${modelLabel}\` [${modelSource}]`,
|
|
1764
|
+
`**Trigger:** ${ctx.group.requiresTrigger ? "required" : "not required"}`
|
|
1765
|
+
];
|
|
1766
|
+
if (managed) {
|
|
1767
|
+
const s = managed.session;
|
|
1768
|
+
const actualModel = s.model;
|
|
1769
|
+
const actualLabel = actualModel ? `${actualModel.provider}/${actualModel.id}` : "none";
|
|
1770
|
+
const ctx$ = s.getContextUsage();
|
|
1771
|
+
lines.push("", `**Session:** \`${s.sessionId}\``, actualLabel !== modelLabel ? `**Session model:** \`${actualLabel}\` ⚠️ mismatch` : `**Session model:** \`${actualLabel}\``, `**Messages:** ${s.messages.length}`, `**Streaming:** ${s.isStreaming ? "yes" : "no"}`, `**Idle:** ${formatDuration(Date.now() - managed.lastActivity)}`);
|
|
1772
|
+
if (ctx$?.tokens != null && ctx$.percent != null) {
|
|
1773
|
+
const pct = Math.round(ctx$.percent);
|
|
1774
|
+
lines.push(`**Context:** ${ctx$.tokens.toLocaleString()} / ${ctx$.contextWindow.toLocaleString()} tokens (${pct}%)`);
|
|
1775
|
+
}
|
|
1776
|
+
} else lines.push("", "*No active session*");
|
|
1777
|
+
const allGroups = ctx.server.getRegisteredGroups();
|
|
1778
|
+
const allTasks = await getAllTasks();
|
|
1779
|
+
lines.push("", "---", "", "**All Groups:**");
|
|
1780
|
+
for (const [jid, g] of Object.entries(allGroups)) {
|
|
1781
|
+
const gResolved = resolveGroupModel(g);
|
|
1782
|
+
const gModelLabel = gResolved ? `${gResolved.provider}/${gResolved.id}` : "none";
|
|
1783
|
+
const gManaged = sessions.get(jid);
|
|
1784
|
+
const sessionPart = gManaged ? `active, idle ${formatDuration(Date.now() - gManaged.lastActivity)}` : "no session";
|
|
1785
|
+
const soulPart = g.soul ? " 🧬" : "";
|
|
1786
|
+
lines.push(`- **${g.name}** (\`${g.folder}\`) — \`${gModelLabel}\` — ${sessionPart}${soulPart}`);
|
|
1787
|
+
}
|
|
1788
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - 1440 * 60 * 1e3)).toISOString();
|
|
1789
|
+
const recentTasks = allTasks.filter((t) => t.status === "active" || t.last_run && t.last_run > cutoff);
|
|
1790
|
+
if (recentTasks.length > 0) {
|
|
1791
|
+
lines.push("", `**Tasks** (active or run in last 24h): ${recentTasks.length}`);
|
|
1792
|
+
for (const t of recentTasks.slice(0, 10)) {
|
|
1793
|
+
const groupName = Object.values(allGroups).find((g) => g.folder === t.group_folder)?.name || t.group_folder;
|
|
1794
|
+
lines.push(`- [${t.status}] \`${t.schedule_type}:${t.schedule_value}\` in **${groupName}** — ${t.prompt.slice(0, 60)}`);
|
|
1795
|
+
}
|
|
1796
|
+
if (recentTasks.length > 10) lines.push(` … and ${recentTasks.length - 10} more`);
|
|
1797
|
+
}
|
|
1798
|
+
lines.push("", `*${Object.keys(allGroups).length} groups, ${allTasks.filter((t) => t.status === "active").length} active tasks*`);
|
|
1799
|
+
await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
|
|
1800
|
+
}
|
|
1801
|
+
function formatDuration(ms) {
|
|
1802
|
+
const s = Math.floor(ms / 1e3);
|
|
1803
|
+
if (s < 60) return `${s}s`;
|
|
1804
|
+
const m = Math.floor(s / 60);
|
|
1805
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
1806
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
1807
|
+
}
|
|
1808
|
+
async function handleNew(ctx) {
|
|
1809
|
+
ctx.server.pi.kill(ctx.chatJid);
|
|
1810
|
+
await removeSession(ctx.group.folder);
|
|
1811
|
+
const sessionDir = path$1.join(SESSIONS_DIR, ctx.group.folder);
|
|
1812
|
+
fs$1.rmSync(sessionDir, {
|
|
1813
|
+
recursive: true,
|
|
1814
|
+
force: true
|
|
1815
|
+
});
|
|
1816
|
+
delete ctx.server.sessions[ctx.group.folder];
|
|
1817
|
+
await clearMessages(ctx.chatJid);
|
|
1818
|
+
await ctx.server.sendBotMessage(ctx.chatJid, "Session reset. Next message starts a fresh conversation.");
|
|
1819
|
+
}
|
|
1820
|
+
async function handleStop(ctx) {
|
|
1821
|
+
if (!sessions.get(ctx.chatJid)) return;
|
|
1822
|
+
await ctx.server.pi.kill(ctx.chatJid);
|
|
1823
|
+
}
|
|
1824
|
+
async function handleModel(ctx, arg) {
|
|
1825
|
+
if (!arg) return listModels(ctx);
|
|
1826
|
+
if (arg === "default" || arg === "reset") return resetModel(ctx);
|
|
1827
|
+
return setModel(ctx, arg);
|
|
1828
|
+
}
|
|
1829
|
+
async function listModels(ctx) {
|
|
1830
|
+
const available = getFilteredModels();
|
|
1831
|
+
const resolved = resolveGroupModel(ctx.group);
|
|
1832
|
+
const lines = [
|
|
1833
|
+
`**Current:** \`${ctx.group.model || "default (global)"}\` → \`${resolved ? `${resolved.provider}/${resolved.id}` : "none"}\``,
|
|
1834
|
+
"",
|
|
1835
|
+
"**Available models:**"
|
|
1836
|
+
];
|
|
1837
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
1838
|
+
for (const m of available) {
|
|
1839
|
+
let list = byProvider.get(m.provider);
|
|
1840
|
+
if (!list) {
|
|
1841
|
+
list = [];
|
|
1842
|
+
byProvider.set(m.provider, list);
|
|
1843
|
+
}
|
|
1844
|
+
const marker = resolved && m.provider === resolved.provider && m.id === resolved.id ? " ←" : "";
|
|
1845
|
+
list.push(`- \`${m.provider}/${m.id}\`${marker}`);
|
|
1846
|
+
}
|
|
1847
|
+
for (const [provider, models] of byProvider) {
|
|
1848
|
+
lines.push("", `**${provider}**`);
|
|
1849
|
+
lines.push(...models);
|
|
1850
|
+
}
|
|
1851
|
+
lines.push("", "`/model <query>` | `/model default`");
|
|
1852
|
+
await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
|
|
1853
|
+
}
|
|
1854
|
+
async function resetModel(ctx) {
|
|
1855
|
+
const had = ctx.group.model;
|
|
1856
|
+
await updateGroupModel(ctx, void 0);
|
|
1857
|
+
await ctx.server.sendBotMessage(ctx.chatJid, had ? `Model reset to default (was: \`${had}\`).` : "Already using default model.");
|
|
1858
|
+
}
|
|
1859
|
+
async function setModel(ctx, query) {
|
|
1860
|
+
const match = findModel(query);
|
|
1861
|
+
if (!match) {
|
|
1862
|
+
await ctx.server.sendBotMessage(ctx.chatJid, `No model matching "${query}". Use \`/model\` to list available models.`);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const modelStr = `${match.provider}/${match.id}`;
|
|
1866
|
+
await updateGroupModel(ctx, modelStr);
|
|
1867
|
+
await ctx.server.sendBotMessage(ctx.chatJid, `Model set to \`${modelStr}\``);
|
|
1868
|
+
}
|
|
1869
|
+
async function updateGroupModel(ctx, model) {
|
|
1870
|
+
const old = ctx.group.model;
|
|
1871
|
+
ctx.group.model = model;
|
|
1872
|
+
await ctx.server.setRegisteredGroup(ctx.chatJid, ctx.group);
|
|
1873
|
+
if (old !== model) ctx.server.pi.kill(ctx.chatJid);
|
|
1874
|
+
}
|
|
1875
|
+
async function handleThinking(ctx, arg) {
|
|
1876
|
+
if (!arg) {
|
|
1877
|
+
const current = ctx.group.thinkingLevel || "off";
|
|
1878
|
+
await ctx.server.sendBotMessage(ctx.chatJid, `**Thinking level:** \`${current}\`\n\n\`/thinking <level>\`\nLevels: ${VALID_THINKING_LEVELS.map((l) => `\`${l}\``).join(", ")}`);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const level = arg.toLowerCase();
|
|
1882
|
+
if (!VALID_THINKING_LEVELS.includes(level)) {
|
|
1883
|
+
await ctx.server.sendBotMessage(ctx.chatJid, `Invalid thinking level "${arg}". Must be one of: ${VALID_THINKING_LEVELS.map((l) => `\`${l}\``).join(", ")}`);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const old = ctx.group.thinkingLevel;
|
|
1887
|
+
ctx.group.thinkingLevel = level === "off" ? void 0 : level;
|
|
1888
|
+
await ctx.server.setRegisteredGroup(ctx.chatJid, ctx.group);
|
|
1889
|
+
if (old !== ctx.group.thinkingLevel) ctx.server.pi.kill(ctx.chatJid);
|
|
1890
|
+
await ctx.server.sendBotMessage(ctx.chatJid, `Thinking level set to \`${level}\``);
|
|
1891
|
+
}
|
|
1892
|
+
async function handleCompact(ctx, arg) {
|
|
1893
|
+
const managed = sessions.get(ctx.chatJid);
|
|
1894
|
+
if (!managed) {
|
|
1895
|
+
await ctx.server.sendBotMessage(ctx.chatJid, "No active session to compact. Send a message first, then run `/compact`.");
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
const session = managed.session;
|
|
1899
|
+
if (session.isCompacting) {
|
|
1900
|
+
await ctx.server.sendBotMessage(ctx.chatJid, "Compaction is already in progress.");
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
const before = session.getContextUsage();
|
|
1904
|
+
await ctx.server.sendBotMessage(ctx.chatJid, "Compacting session context…");
|
|
1905
|
+
const result = await session.compact(arg || void 0);
|
|
1906
|
+
const after = session.getContextUsage();
|
|
1907
|
+
const lines = ["**Compaction complete.**"];
|
|
1908
|
+
if (typeof result?.tokensBefore === "number") lines.push(`**Tokens before:** ${result.tokensBefore.toLocaleString()}`);
|
|
1909
|
+
if (before?.tokens != null) lines.push(`**Est. before:** ${before.tokens.toLocaleString()}`);
|
|
1910
|
+
if (after?.tokens != null) lines.push(`**Est. after:** ${after.tokens.toLocaleString()}`);
|
|
1911
|
+
await ctx.server.sendBotMessage(ctx.chatJid, lines.join("\n"));
|
|
1912
|
+
}
|
|
1913
|
+
function findModel(query) {
|
|
1914
|
+
const available = getFilteredModels();
|
|
1915
|
+
if (available.length === 0) return void 0;
|
|
1916
|
+
const q = query.toLowerCase();
|
|
1917
|
+
const exact = available.filter((m) => `${m.provider}/${m.id}`.toLowerCase() === q);
|
|
1918
|
+
if (exact.length === 1) return exact[0];
|
|
1919
|
+
const byId = available.filter((m) => m.id.toLowerCase() === q);
|
|
1920
|
+
if (byId.length === 1) return byId[0];
|
|
1921
|
+
const tokens = q.split(/[\s/]+/).filter(Boolean);
|
|
1922
|
+
const fuzzy = available.filter((m) => {
|
|
1923
|
+
const label = `${m.provider}/${m.id}`.toLowerCase();
|
|
1924
|
+
return tokens.every((t) => label.includes(t));
|
|
1925
|
+
});
|
|
1926
|
+
const matches = exact.length > 1 ? exact : byId.length > 1 ? byId : fuzzy;
|
|
1927
|
+
if (matches.length === 0) return void 0;
|
|
1928
|
+
matches.sort((a, b) => a.id.length - b.id.length || b.id.localeCompare(a.id, void 0, { numeric: true }));
|
|
1929
|
+
return matches[0];
|
|
1930
|
+
}
|
|
1931
|
+
new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
|
|
16
1932
|
await init();
|
|
17
1933
|
var logger$3 = createLogger("telegram");
|
|
18
1934
|
var JID_PREFIX = "tg:";
|
|
19
1935
|
var POLL_INTERVAL = 1e3;
|
|
20
1936
|
var POLL_TIMEOUT = 30;
|
|
21
1937
|
var API_BASE = "https://api.telegram.org/bot";
|
|
22
|
-
var STREAM_THROTTLE_MIN =
|
|
23
|
-
var STREAM_THROTTLE_MAX =
|
|
24
|
-
var STREAM_THROTTLE_STEP =
|
|
1938
|
+
var STREAM_THROTTLE_MIN = 1e3;
|
|
1939
|
+
var STREAM_THROTTLE_MAX = 1e4;
|
|
1940
|
+
var STREAM_THROTTLE_STEP = 1e3;
|
|
25
1941
|
var STREAM_THROTTLE_JITTER = 120;
|
|
26
1942
|
var STREAM_INITIAL_DELAY = 100;
|
|
27
1943
|
var TG_MAX_TEXT = 4096;
|
|
@@ -61,6 +1977,7 @@ var TelegramChannel = class {
|
|
|
61
1977
|
const state = this.streamState.get(jid);
|
|
62
1978
|
const { text: formatted, parse_mode } = formatForTelegram(text);
|
|
63
1979
|
const parseMode = { parse_mode };
|
|
1980
|
+
const plainText = stripThinkingTags(text);
|
|
64
1981
|
if (state) {
|
|
65
1982
|
this.streamState.delete(jid);
|
|
66
1983
|
if (state.editTimer) clearTimeout(state.editTimer);
|
|
@@ -73,13 +1990,43 @@ var TelegramChannel = class {
|
|
|
73
1990
|
...parseMode
|
|
74
1991
|
});
|
|
75
1992
|
return;
|
|
76
|
-
} catch {
|
|
1993
|
+
} catch (editErr) {
|
|
1994
|
+
if (isTgParseError(editErr)) try {
|
|
1995
|
+
logger$3.debug({
|
|
1996
|
+
err: editErr,
|
|
1997
|
+
formattedText: formatted.slice(0, 500),
|
|
1998
|
+
originalText: text.slice(0, 500)
|
|
1999
|
+
}, "HTML edit failed, retrying as plain text");
|
|
2000
|
+
await this.api("editMessageText", {
|
|
2001
|
+
chat_id: chatId,
|
|
2002
|
+
message_id: state.messageId,
|
|
2003
|
+
text: plainText.slice(0, TG_MAX_TEXT)
|
|
2004
|
+
});
|
|
2005
|
+
return;
|
|
2006
|
+
} catch (plainErr) {
|
|
2007
|
+
logger$3.debug({ err: plainErr }, "Plain-text edit also failed, sending new message");
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
try {
|
|
2012
|
+
await this.api("sendMessage", {
|
|
2013
|
+
chat_id: chatId,
|
|
2014
|
+
text: formatted,
|
|
2015
|
+
...parseMode
|
|
2016
|
+
});
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
if (isTgParseError(err)) {
|
|
2019
|
+
logger$3.debug({
|
|
2020
|
+
err,
|
|
2021
|
+
formattedText: formatted.slice(0, 500),
|
|
2022
|
+
originalText: text.slice(0, 500)
|
|
2023
|
+
}, "HTML send failed, retrying as plain text");
|
|
2024
|
+
await this.api("sendMessage", {
|
|
2025
|
+
chat_id: chatId,
|
|
2026
|
+
text: plainText
|
|
2027
|
+
});
|
|
2028
|
+
} else throw err;
|
|
77
2029
|
}
|
|
78
|
-
await this.api("sendMessage", {
|
|
79
|
-
chat_id: chatId,
|
|
80
|
-
text: formatted,
|
|
81
|
-
...parseMode
|
|
82
|
-
});
|
|
83
2030
|
}
|
|
84
2031
|
isConnected() {
|
|
85
2032
|
return this.connected;
|
|
@@ -113,16 +2060,69 @@ var TelegramChannel = class {
|
|
|
113
2060
|
switch (event.type) {
|
|
114
2061
|
case "thinking_delta": {
|
|
115
2062
|
const state = this.getOrCreateStreamState(jid);
|
|
116
|
-
|
|
2063
|
+
const last = state.details[state.details.length - 1];
|
|
2064
|
+
if (last && last.type === "thinking") last.text += event.delta || "";
|
|
2065
|
+
else state.details.push({
|
|
2066
|
+
type: "thinking",
|
|
2067
|
+
text: event.delta || ""
|
|
2068
|
+
});
|
|
117
2069
|
this.scheduleStreamEdit(jid, state);
|
|
118
2070
|
break;
|
|
119
2071
|
}
|
|
120
2072
|
case "text_delta": {
|
|
121
2073
|
const state = this.getOrCreateStreamState(jid);
|
|
122
|
-
|
|
2074
|
+
const last = state.details[state.details.length - 1];
|
|
2075
|
+
if (last && last.type === "text") last.text += event.delta || "";
|
|
2076
|
+
else state.details.push({
|
|
2077
|
+
type: "text",
|
|
2078
|
+
text: event.delta || ""
|
|
2079
|
+
});
|
|
2080
|
+
this.scheduleStreamEdit(jid, state);
|
|
2081
|
+
break;
|
|
2082
|
+
}
|
|
2083
|
+
case "tool_start": {
|
|
2084
|
+
const state = this.getOrCreateStreamState(jid);
|
|
2085
|
+
const tool = {
|
|
2086
|
+
name: event.toolName || "tool",
|
|
2087
|
+
done: false
|
|
2088
|
+
};
|
|
2089
|
+
state.tools.push(tool);
|
|
2090
|
+
state.details.push({
|
|
2091
|
+
type: "tool",
|
|
2092
|
+
tool
|
|
2093
|
+
});
|
|
123
2094
|
this.scheduleStreamEdit(jid, state);
|
|
124
2095
|
break;
|
|
125
2096
|
}
|
|
2097
|
+
case "tool_end": {
|
|
2098
|
+
const state = this.getOrCreateStreamState(jid);
|
|
2099
|
+
for (let i = state.tools.length - 1; i >= 0; i--) {
|
|
2100
|
+
const t = state.tools[i];
|
|
2101
|
+
if (t.name === event.toolName && !t.done) {
|
|
2102
|
+
t.done = true;
|
|
2103
|
+
t.isError = event.isError;
|
|
2104
|
+
t.resultPreview = event.resultPreview;
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
this.scheduleStreamEdit(jid, state);
|
|
2109
|
+
break;
|
|
2110
|
+
}
|
|
2111
|
+
case "error": {
|
|
2112
|
+
const state = this.getOrCreateStreamState(jid);
|
|
2113
|
+
if (state.editTimer) {
|
|
2114
|
+
clearTimeout(state.editTimer);
|
|
2115
|
+
state.editTimer = null;
|
|
2116
|
+
}
|
|
2117
|
+
const errorMsg = event.error || "Unknown error";
|
|
2118
|
+
const chatId = jid.slice(3);
|
|
2119
|
+
this.api("sendMessage", {
|
|
2120
|
+
chat_id: chatId,
|
|
2121
|
+
text: `⚠️ ${errorMsg}`
|
|
2122
|
+
}).catch(() => {});
|
|
2123
|
+
this.streamState.delete(jid);
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
126
2126
|
case "text_end":
|
|
127
2127
|
case "agent_end": {
|
|
128
2128
|
const state = this.streamState.get(jid);
|
|
@@ -139,8 +2139,8 @@ var TelegramChannel = class {
|
|
|
139
2139
|
if (!state) {
|
|
140
2140
|
state = {
|
|
141
2141
|
chatId: jid.slice(3),
|
|
142
|
-
|
|
143
|
-
|
|
2142
|
+
details: [],
|
|
2143
|
+
tools: [],
|
|
144
2144
|
messageId: null,
|
|
145
2145
|
sendPromise: null,
|
|
146
2146
|
lastEdit: 0,
|
|
@@ -162,42 +2162,83 @@ var TelegramChannel = class {
|
|
|
162
2162
|
}, delay);
|
|
163
2163
|
}
|
|
164
2164
|
async flushStreamEdit(jid, state) {
|
|
2165
|
+
if (!this.streamState.has(jid)) return;
|
|
165
2166
|
if (state.sendPromise) {
|
|
166
2167
|
await state.sendPromise.catch(() => {});
|
|
167
2168
|
state.sendPromise = null;
|
|
168
2169
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
2170
|
+
if (!this.streamState.has(jid)) return;
|
|
2171
|
+
const htmlParts = [];
|
|
2172
|
+
let toolBuf = [];
|
|
2173
|
+
const flushTools = () => {
|
|
2174
|
+
if (!toolBuf.length) return;
|
|
2175
|
+
htmlParts.push(formatToolLines(toolBuf));
|
|
2176
|
+
toolBuf = [];
|
|
2177
|
+
};
|
|
2178
|
+
for (const item of state.details) if (item.type === "tool") toolBuf.push(item.tool);
|
|
2179
|
+
else if (item.type === "thinking") {
|
|
2180
|
+
flushTools();
|
|
2181
|
+
const trimmed = item.text.trim().replace(/\*\*/g, "");
|
|
2182
|
+
if (trimmed) htmlParts.push(`💭 <b>${escHtml(trimmed)}</b>`);
|
|
2183
|
+
} else {
|
|
2184
|
+
flushTools();
|
|
2185
|
+
const clean = stripInternalTags(item.text).trim();
|
|
2186
|
+
if (clean) {
|
|
2187
|
+
const truncated = clean.length > TG_MAX_TEXT ? clean.slice(0, TG_MAX_TEXT - 1) + "…" : clean;
|
|
2188
|
+
htmlParts.push(formatForTelegram(truncated).text);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
flushTools();
|
|
2192
|
+
if (!htmlParts.length) return;
|
|
2193
|
+
htmlParts.push("⏳");
|
|
2194
|
+
const formatted = htmlParts.join("\n\n");
|
|
2195
|
+
const parse_mode = "HTML";
|
|
2196
|
+
const snapshotLen = detailsTextLen(state.details);
|
|
2197
|
+
const streamApiOpts = { maxRetryWait: 5 };
|
|
2198
|
+
const sendOrEdit = async (text, parseMode) => {
|
|
2199
|
+
if (!this.streamState.has(jid)) return;
|
|
2200
|
+
const body = {
|
|
2201
|
+
chat_id: state.chatId,
|
|
2202
|
+
text
|
|
2203
|
+
};
|
|
2204
|
+
if (parseMode) body.parse_mode = parseMode;
|
|
174
2205
|
if (state.messageId === null) {
|
|
175
|
-
state.sendPromise = this.api("sendMessage", {
|
|
176
|
-
chat_id: state.chatId,
|
|
177
|
-
text: formatted,
|
|
178
|
-
parse_mode
|
|
179
|
-
}).then((msg) => {
|
|
2206
|
+
state.sendPromise = this.api("sendMessage", body, streamApiOpts).then((msg) => {
|
|
180
2207
|
state.messageId = msg.message_id;
|
|
181
2208
|
state.sendPromise = null;
|
|
182
2209
|
});
|
|
183
2210
|
await state.sendPromise;
|
|
184
|
-
} else
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
2211
|
+
} else {
|
|
2212
|
+
body.message_id = state.messageId;
|
|
2213
|
+
await this.api("editMessageText", body, streamApiOpts);
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
try {
|
|
2217
|
+
await sendOrEdit(formatted, parse_mode);
|
|
190
2218
|
state.lastEdit = Date.now();
|
|
191
2219
|
state.throttleMs = Math.max(STREAM_THROTTLE_MIN, state.throttleMs - 50);
|
|
192
2220
|
} catch (err) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
2221
|
+
if (isTgParseError(err)) {
|
|
2222
|
+
logger$3.debug({
|
|
2223
|
+
err,
|
|
2224
|
+
formattedText: formatted.slice(0, 500)
|
|
2225
|
+
}, "Stream HTML failed, retrying as plain text");
|
|
2226
|
+
try {
|
|
2227
|
+
await sendOrEdit(stripHtmlTags(formatted).slice(0, TG_MAX_TEXT));
|
|
2228
|
+
state.lastEdit = Date.now();
|
|
2229
|
+
} catch (retryErr) {
|
|
2230
|
+
logger$3.debug({ err: retryErr }, "Stream plain-text fallback also failed");
|
|
2231
|
+
}
|
|
2232
|
+
} else if (isTgMessageNotModified(err)) {} else {
|
|
2233
|
+
const retryAfterMs = getRetryAfterMs(err);
|
|
2234
|
+
if (retryAfterMs > 0) state.throttleMs = Math.min(STREAM_THROTTLE_MAX, Math.max(state.throttleMs + STREAM_THROTTLE_STEP, retryAfterMs));
|
|
2235
|
+
logger$3.debug({
|
|
2236
|
+
err,
|
|
2237
|
+
throttleMs: state.throttleMs
|
|
2238
|
+
}, "Stream edit failed");
|
|
2239
|
+
}
|
|
199
2240
|
}
|
|
200
|
-
if ((state.
|
|
2241
|
+
if (detailsTextLen(state.details) > snapshotLen && this.streamState.has(jid)) this.scheduleStreamEdit(jid, state);
|
|
201
2242
|
}
|
|
202
2243
|
startPolling() {
|
|
203
2244
|
this.stopPolling();
|
|
@@ -219,17 +2260,17 @@ var TelegramChannel = class {
|
|
|
219
2260
|
const res = await fetch(`${API_BASE}${this.config.botToken}/getUpdates?${params}`, { signal });
|
|
220
2261
|
if (!res.ok) {
|
|
221
2262
|
if (res.status === 409) {
|
|
222
|
-
logger$3.
|
|
2263
|
+
logger$3.debug("Telegram 409 conflict: another polling instance is active, retrying in 30s");
|
|
223
2264
|
await sleep(3e4, signal);
|
|
224
2265
|
continue;
|
|
225
2266
|
}
|
|
226
|
-
logger$3.
|
|
2267
|
+
logger$3.debug({ status: res.status }, "Telegram getUpdates HTTP error");
|
|
227
2268
|
await sleep(POLL_INTERVAL, signal);
|
|
228
2269
|
continue;
|
|
229
2270
|
}
|
|
230
2271
|
const data = await res.json();
|
|
231
2272
|
if (!data.ok) {
|
|
232
|
-
logger$3.
|
|
2273
|
+
logger$3.debug({ description: data.description }, "Telegram getUpdates API error");
|
|
233
2274
|
await sleep(POLL_INTERVAL, signal);
|
|
234
2275
|
continue;
|
|
235
2276
|
}
|
|
@@ -240,7 +2281,7 @@ var TelegramChannel = class {
|
|
|
240
2281
|
}
|
|
241
2282
|
} catch (err) {
|
|
242
2283
|
if (signal.aborted) break;
|
|
243
|
-
logger$3.
|
|
2284
|
+
logger$3.debug({ err }, "Telegram polling error");
|
|
244
2285
|
await sleep(POLL_INTERVAL, signal);
|
|
245
2286
|
}
|
|
246
2287
|
};
|
|
@@ -250,37 +2291,120 @@ var TelegramChannel = class {
|
|
|
250
2291
|
this.pollAbort?.abort();
|
|
251
2292
|
this.pollAbort = null;
|
|
252
2293
|
}
|
|
253
|
-
async api(method, body) {
|
|
2294
|
+
async api(method, body, opts, _retries = 0) {
|
|
254
2295
|
const res = await fetch(`${API_BASE}${this.config.botToken}/${method}`, {
|
|
255
2296
|
method: "POST",
|
|
256
2297
|
headers: { "content-type": "application/json" },
|
|
257
2298
|
body: body ? JSON.stringify(body) : void 0
|
|
258
2299
|
});
|
|
259
2300
|
const data = await res.json();
|
|
260
|
-
if (!data.ok)
|
|
2301
|
+
if (!data.ok) {
|
|
2302
|
+
if (res.status === 429 && _retries < 2) {
|
|
2303
|
+
const retryAfter = Number(data.parameters?.retry_after || 5);
|
|
2304
|
+
const maxWait = opts?.maxRetryWait ?? 30;
|
|
2305
|
+
if (retryAfter > maxWait) {
|
|
2306
|
+
logger$3.debug({
|
|
2307
|
+
method,
|
|
2308
|
+
retryAfter,
|
|
2309
|
+
maxWait
|
|
2310
|
+
}, "Telegram 429 retry_after exceeds max wait, skipping retry");
|
|
2311
|
+
throw new TelegramApiError(method, data.description || "unknown error", res.status, data);
|
|
2312
|
+
}
|
|
2313
|
+
logger$3.debug({
|
|
2314
|
+
method,
|
|
2315
|
+
retryAfter,
|
|
2316
|
+
attempt: _retries + 1
|
|
2317
|
+
}, "Telegram 429 rate limit, waiting");
|
|
2318
|
+
await sleep(retryAfter * 1e3);
|
|
2319
|
+
return this.api(method, body, opts, _retries + 1);
|
|
2320
|
+
}
|
|
2321
|
+
throw new TelegramApiError(method, data.description || "unknown error", res.status, data);
|
|
2322
|
+
}
|
|
261
2323
|
return data.result;
|
|
262
2324
|
}
|
|
263
2325
|
handleUpdate(update) {
|
|
264
2326
|
const message = update.message || update.edited_message || update.channel_post;
|
|
265
|
-
if (!message
|
|
2327
|
+
if (!message) return;
|
|
2328
|
+
if (!(message.text || message.caption || message.photo || message.document || message.voice || message.audio || message.video || message.video_note || message.sticker)) return;
|
|
266
2329
|
if (message.from?.id === this.botId) return;
|
|
267
2330
|
const chatId = String(message.chat.id);
|
|
268
2331
|
const jid = `${JID_PREFIX}${chatId}`;
|
|
269
2332
|
const isGroup = message.chat.type === "group" || message.chat.type === "supergroup";
|
|
270
2333
|
const senderName = message.from ? [message.from.first_name, message.from.last_name].filter(Boolean).join(" ") : message.chat.title || chatId;
|
|
271
2334
|
const now = (/* @__PURE__ */ new Date(message.date * 1e3)).toISOString();
|
|
2335
|
+
const msgId = crypto.randomUUID();
|
|
272
2336
|
const msg = {
|
|
273
|
-
id:
|
|
2337
|
+
id: msgId,
|
|
274
2338
|
chat_jid: jid,
|
|
275
2339
|
sender: String(message.from?.id || chatId),
|
|
276
2340
|
sender_name: senderName,
|
|
277
|
-
content: message.text,
|
|
2341
|
+
content: message.text || message.caption || "",
|
|
278
2342
|
timestamp: now,
|
|
279
2343
|
is_from_me: false,
|
|
280
2344
|
is_bot_message: false
|
|
281
2345
|
};
|
|
282
|
-
this.opts.
|
|
283
|
-
this.
|
|
2346
|
+
const group = this.opts.registeredGroups()[jid];
|
|
2347
|
+
(group ? this.downloadMedia(message, msgId, group.folder) : Promise.resolve([])).then((attachments) => {
|
|
2348
|
+
if (attachments.length) msg.meta = { attachments };
|
|
2349
|
+
this.opts.onMessage(jid, msg);
|
|
2350
|
+
this.opts.onChatMetadata(jid, now, senderName, "telegram", isGroup);
|
|
2351
|
+
}).catch((err) => {
|
|
2352
|
+
logger$3.debug({ err }, "Failed to download Telegram media, sending text-only");
|
|
2353
|
+
this.opts.onMessage(jid, msg);
|
|
2354
|
+
this.opts.onChatMetadata(jid, now, senderName, "telegram", isGroup);
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
async downloadMedia(message, msgId, groupFolder) {
|
|
2358
|
+
const attachments = [];
|
|
2359
|
+
if (message.photo?.length) {
|
|
2360
|
+
const photo = message.photo[message.photo.length - 1];
|
|
2361
|
+
const att = await this.downloadTelegramFile(photo.file_id, groupFolder, msgId, "photo.jpg", "image/jpeg");
|
|
2362
|
+
if (att) attachments.push(att);
|
|
2363
|
+
}
|
|
2364
|
+
if (message.document) {
|
|
2365
|
+
const att = await this.downloadTelegramFile(message.document.file_id, groupFolder, msgId, message.document.file_name || "document", message.document.mime_type || getMimeType(message.document.file_name || "document"));
|
|
2366
|
+
if (att) attachments.push(att);
|
|
2367
|
+
}
|
|
2368
|
+
if (message.voice) {
|
|
2369
|
+
const att = await this.downloadTelegramFile(message.voice.file_id, groupFolder, msgId, "voice.ogg", message.voice.mime_type || "audio/ogg");
|
|
2370
|
+
if (att) attachments.push(att);
|
|
2371
|
+
}
|
|
2372
|
+
if (message.audio) {
|
|
2373
|
+
const att = await this.downloadTelegramFile(message.audio.file_id, groupFolder, msgId, message.audio.file_name || "audio.mp3", message.audio.mime_type || "audio/mpeg");
|
|
2374
|
+
if (att) attachments.push(att);
|
|
2375
|
+
}
|
|
2376
|
+
if (message.video) {
|
|
2377
|
+
const att = await this.downloadTelegramFile(message.video.file_id, groupFolder, msgId, message.video.file_name || "video.mp4", message.video.mime_type || "video/mp4");
|
|
2378
|
+
if (att) attachments.push(att);
|
|
2379
|
+
}
|
|
2380
|
+
if (message.video_note) {
|
|
2381
|
+
const att = await this.downloadTelegramFile(message.video_note.file_id, groupFolder, msgId, "video_note.mp4", "video/mp4");
|
|
2382
|
+
if (att) attachments.push(att);
|
|
2383
|
+
}
|
|
2384
|
+
if (message.sticker && !message.sticker.is_animated) {
|
|
2385
|
+
const att = await this.downloadTelegramFile(message.sticker.file_id, groupFolder, msgId, "sticker.webp", "image/webp");
|
|
2386
|
+
if (att) attachments.push(att);
|
|
2387
|
+
}
|
|
2388
|
+
return attachments;
|
|
2389
|
+
}
|
|
2390
|
+
async downloadTelegramFile(fileId, groupFolder, msgId, name, mimeType) {
|
|
2391
|
+
try {
|
|
2392
|
+
const fileInfo = await this.api("getFile", { file_id: fileId });
|
|
2393
|
+
const url = `https://api.telegram.org/file/bot${this.config.botToken}/${fileInfo.file_path}`;
|
|
2394
|
+
const res = await fetch(url);
|
|
2395
|
+
if (!res.ok) return null;
|
|
2396
|
+
return saveAttachment(groupFolder, msgId, {
|
|
2397
|
+
data: Buffer.from(await res.arrayBuffer()).toString("base64"),
|
|
2398
|
+
mimeType,
|
|
2399
|
+
name
|
|
2400
|
+
});
|
|
2401
|
+
} catch (err) {
|
|
2402
|
+
logger$3.debug({
|
|
2403
|
+
err,
|
|
2404
|
+
fileId
|
|
2405
|
+
}, "Failed to download Telegram file");
|
|
2406
|
+
return null;
|
|
2407
|
+
}
|
|
284
2408
|
}
|
|
285
2409
|
};
|
|
286
2410
|
var TelegramApiError = class extends Error {
|
|
@@ -291,39 +2415,28 @@ var TelegramApiError = class extends Error {
|
|
|
291
2415
|
this.response = response;
|
|
292
2416
|
}
|
|
293
2417
|
};
|
|
294
|
-
/** Format message text as Telegram
|
|
2418
|
+
/** Format message text as Telegram HTML — parse markdown with md4x, render AST to HTML tags */
|
|
295
2419
|
function formatForTelegram(text) {
|
|
296
2420
|
let remaining = text.replace(/<\/?internal>/g, (t) => t.replace("internal", "thinking"));
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
let m;
|
|
301
|
-
while ((m = thinkingRe.exec(remaining)) !== null) {
|
|
302
|
-
if (m.index > lastIdx) parts.push(mdToTgV2(remaining.slice(lastIdx, m.index)));
|
|
303
|
-
const trimmed = (m[1] || "").trim();
|
|
304
|
-
if (trimmed) {
|
|
305
|
-
const quoted = escTgV2(trimmed).split("\n").map((l) => `>${l}`).join("\n");
|
|
306
|
-
parts.push(quoted);
|
|
307
|
-
}
|
|
308
|
-
lastIdx = m.index + m[0].length;
|
|
309
|
-
}
|
|
310
|
-
if (lastIdx < remaining.length) parts.push(mdToTgV2(remaining.slice(lastIdx)));
|
|
2421
|
+
remaining = remaining.replace(/<tool\s+name="[^"]*"\s+status="[^"]*"(?:\s+preview="[^"]*")?\s*\/>/g, "");
|
|
2422
|
+
remaining = remaining.replace(/<details><summary>thinking<\/summary>[\s\S]*?<\/details>/g, "");
|
|
2423
|
+
remaining = remaining.replace(/<thinking>[\s\S]*?<\/thinking>/g, "");
|
|
311
2424
|
return {
|
|
312
|
-
text:
|
|
313
|
-
parse_mode: "
|
|
2425
|
+
text: [mdToTgHtml(remaining)].join("").replace(/\n{3,}/g, "\n\n").trim(),
|
|
2426
|
+
parse_mode: "HTML"
|
|
314
2427
|
};
|
|
315
2428
|
}
|
|
316
|
-
/** Parse markdown with md4x and render AST nodes to Telegram
|
|
317
|
-
function
|
|
318
|
-
return parseAST(text).nodes.map((node) => renderNode(node
|
|
2429
|
+
/** Parse markdown with md4x and render AST nodes to Telegram HTML */
|
|
2430
|
+
function mdToTgHtml(text) {
|
|
2431
|
+
return parseAST(text).nodes.map((node) => renderNode(node)).join("\n\n");
|
|
319
2432
|
}
|
|
320
|
-
/** Collect raw text from children (for code content that must not be escaped) */
|
|
2433
|
+
/** Collect raw text from children recursively (for code content that must not be escaped) */
|
|
321
2434
|
function collectText(children) {
|
|
322
|
-
return children.map((c) => typeof c === "string" ? c :
|
|
2435
|
+
return children.map((c) => typeof c === "string" ? c : collectText(c.slice(2))).join("");
|
|
323
2436
|
}
|
|
324
|
-
/** Render a single AST node to Telegram
|
|
325
|
-
function renderNode(node
|
|
326
|
-
if (typeof node === "string") return
|
|
2437
|
+
/** Render a single AST node to Telegram HTML */
|
|
2438
|
+
function renderNode(node) {
|
|
2439
|
+
if (typeof node === "string") return escHtml(node);
|
|
327
2440
|
const [tag, attrs, ...children] = node;
|
|
328
2441
|
const inner = () => children.map((c) => renderNode(c)).join("");
|
|
329
2442
|
switch (tag) {
|
|
@@ -333,42 +2446,44 @@ function renderNode(node, topLevel = false) {
|
|
|
333
2446
|
case "h3":
|
|
334
2447
|
case "h4":
|
|
335
2448
|
case "h5":
|
|
336
|
-
case "h6": return
|
|
337
|
-
case "strong": return
|
|
338
|
-
case "em": return
|
|
339
|
-
case "u": return
|
|
340
|
-
case "del": return
|
|
341
|
-
case "code":
|
|
342
|
-
if (!topLevel) return collectText(children);
|
|
343
|
-
return `\`${collectText(children)}\``;
|
|
2449
|
+
case "h6": return `<b>${inner()}</b>`;
|
|
2450
|
+
case "strong": return `<b>${inner()}</b>`;
|
|
2451
|
+
case "em": return `<i>${inner()}</i>`;
|
|
2452
|
+
case "u": return `<u>${inner()}</u>`;
|
|
2453
|
+
case "del": return `<s>${inner()}</s>`;
|
|
2454
|
+
case "code": return `<code>${escHtml(collectText(children))}</code>`;
|
|
344
2455
|
case "pre": {
|
|
345
2456
|
const codeNode = children.find((c) => typeof c !== "string" && c[0] === "code");
|
|
346
|
-
|
|
2457
|
+
const lang = String(attrs.language || "");
|
|
2458
|
+
const raw = codeNode ? collectText(codeNode.slice(2)) : collectText(children);
|
|
2459
|
+
if (!raw.trim() && !lang) return "";
|
|
2460
|
+
return `<pre><code${lang ? ` class="language-${escHtml(lang)}"` : ""}>${escHtml(raw.replace(/\n$/, ""))}</code></pre>`;
|
|
347
2461
|
}
|
|
348
|
-
case "a": {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
case "img": return escTgV2(String(attrs.alt || "image"));
|
|
353
|
-
case "blockquote": return inner().split("\n").map((l) => `>${l}`).join("\n");
|
|
2462
|
+
case "a": return `<a href="${escHtml(String(attrs.href || ""))}">${inner()}</a>`;
|
|
2463
|
+
case "img": return escHtml(String(attrs.alt || "image"));
|
|
2464
|
+
case "blockquote": return `<blockquote>${inner()}</blockquote>`;
|
|
354
2465
|
case "ul":
|
|
355
2466
|
case "ol": return children.map((c, i) => {
|
|
356
|
-
if (typeof c === "string") return
|
|
357
|
-
return (tag === "ol" ? `${
|
|
2467
|
+
if (typeof c === "string") return escHtml(c);
|
|
2468
|
+
return (tag === "ol" ? `${i + 1}. ` : "• ") + renderNode(c);
|
|
358
2469
|
}).filter((l) => l.trim()).join("\n");
|
|
359
2470
|
case "li": return inner();
|
|
360
|
-
case "hr": return
|
|
2471
|
+
case "hr": return "---";
|
|
361
2472
|
case "br": return "\n";
|
|
362
2473
|
default: return inner();
|
|
363
2474
|
}
|
|
364
2475
|
}
|
|
365
|
-
/** Escape
|
|
366
|
-
function
|
|
367
|
-
return s.replace(
|
|
2476
|
+
/** Escape HTML entities for Telegram HTML mode */
|
|
2477
|
+
function escHtml(s) {
|
|
2478
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2479
|
+
}
|
|
2480
|
+
/** Check if a Telegram API error is an HTML parse failure */
|
|
2481
|
+
function isTgParseError(err) {
|
|
2482
|
+
return err instanceof TelegramApiError && err.status === 400 && typeof err.response?.description === "string" && err.response.description.includes("can't parse entities");
|
|
368
2483
|
}
|
|
369
|
-
/**
|
|
370
|
-
function
|
|
371
|
-
return
|
|
2484
|
+
/** Check if a Telegram API error is "message is not modified" (identical content edit) */
|
|
2485
|
+
function isTgMessageNotModified(err) {
|
|
2486
|
+
return err instanceof TelegramApiError && err.status === 400 && typeof err.response?.description === "string" && err.response.description.includes("message is not modified");
|
|
372
2487
|
}
|
|
373
2488
|
function getRetryAfterMs(err) {
|
|
374
2489
|
if (!(err instanceof TelegramApiError)) return 0;
|
|
@@ -376,6 +2491,40 @@ function getRetryAfterMs(err) {
|
|
|
376
2491
|
if (!Number.isFinite(retryAfterSec) || retryAfterSec <= 0) return 0;
|
|
377
2492
|
return retryAfterSec * 1e3;
|
|
378
2493
|
}
|
|
2494
|
+
/** Verbose tool lines with result previews — used during streaming */
|
|
2495
|
+
function formatToolLines(tools) {
|
|
2496
|
+
return tools.map((t) => {
|
|
2497
|
+
const icon = t.done ? t.isError ? "❌" : "✅" : "🔧";
|
|
2498
|
+
const preview = t.done && t.resultPreview ? ` ${tgToolPreview(t.resultPreview)}` : "";
|
|
2499
|
+
return `${icon} ${escHtml(t.name)}${preview}`;
|
|
2500
|
+
}).join("\n");
|
|
2501
|
+
}
|
|
2502
|
+
function detailsTextLen(details) {
|
|
2503
|
+
let len = 0;
|
|
2504
|
+
for (const d of details) len += d.type === "tool" ? 1 : d.text.length;
|
|
2505
|
+
return len;
|
|
2506
|
+
}
|
|
2507
|
+
function tgToolPreview(raw) {
|
|
2508
|
+
const firstLine = extractToolResultText(raw, 200).split("\n")[0] || "";
|
|
2509
|
+
return escHtml(firstLine.length > 80 ? firstLine.slice(0, 79) + "…" : firstLine);
|
|
2510
|
+
}
|
|
2511
|
+
/** Strip <internal>/<thinking> tags from streamed text (handles partial/unclosed tags mid-stream) */
|
|
2512
|
+
function stripInternalTags(text) {
|
|
2513
|
+
let s = text.replace(/<(internal|thinking)>[\s\S]*?<\/\1>/g, "");
|
|
2514
|
+
s = s.replace(/<(internal|thinking)>[\s\S]*$/, "");
|
|
2515
|
+
s = s.replace(/<(?:internal|thinking|intern|inter|inte|int|in|i|thinkin|thinki|think|thin|thi|th|t)?$/i, "");
|
|
2516
|
+
return s;
|
|
2517
|
+
}
|
|
2518
|
+
/** Strip <internal>/<thinking>/tool tags for plain-text fallback — keeps inner content */
|
|
2519
|
+
function stripHtmlTags(html) {
|
|
2520
|
+
return html.replace(/<[^>]*>/g, "");
|
|
2521
|
+
}
|
|
2522
|
+
function stripThinkingTags(text) {
|
|
2523
|
+
let s = text.replace(/<\/?(internal|thinking)>/g, "");
|
|
2524
|
+
s = s.replace(/<details><summary>thinking<\/summary>([\s\S]*?)<\/details>/g, "$1");
|
|
2525
|
+
s = s.replace(/<tool\s+name="([^"]+)"\s+status="[^"]*"(?:\s+preview="[^"]*")?\s*\/>/g, "[$1]");
|
|
2526
|
+
return s;
|
|
2527
|
+
}
|
|
379
2528
|
function randomJitter(maxAbs) {
|
|
380
2529
|
return Math.round((Math.random() * 2 - 1) * maxAbs);
|
|
381
2530
|
}
|
|
@@ -449,16 +2598,18 @@ var GroupQueue = class {
|
|
|
449
2598
|
this.drain();
|
|
450
2599
|
}
|
|
451
2600
|
drain() {
|
|
2601
|
+
const deferred = [];
|
|
452
2602
|
while (this.active.size < config.get("maxConcurrentAgents") && this.queue.length > 0) {
|
|
453
2603
|
const entry = this.queue.shift();
|
|
454
2604
|
if (!entry) break;
|
|
455
2605
|
if (this.active.has(entry.chatJid)) {
|
|
456
|
-
|
|
457
|
-
|
|
2606
|
+
deferred.push(entry);
|
|
2607
|
+
continue;
|
|
458
2608
|
}
|
|
459
2609
|
this.active.add(entry.chatJid);
|
|
460
2610
|
this.processEntry(entry);
|
|
461
2611
|
}
|
|
2612
|
+
this.queue.push(...deferred);
|
|
462
2613
|
}
|
|
463
2614
|
async processEntry(entry) {
|
|
464
2615
|
try {
|
|
@@ -500,7 +2651,14 @@ function escapeXml(s) {
|
|
|
500
2651
|
}
|
|
501
2652
|
/** Format messages into XML for the agent prompt */
|
|
502
2653
|
function formatMessages(messages) {
|
|
503
|
-
return `<messages>\n${messages.map((m) =>
|
|
2654
|
+
return `<messages>\n${messages.map((m) => {
|
|
2655
|
+
let body = escapeXml(m.content);
|
|
2656
|
+
if (m.meta?.attachments?.length) body += m.meta.attachments.map((a) => {
|
|
2657
|
+
const fileAttr = a.mimeType.startsWith("image/") ? "" : ` file="downloads/${a.path.split("/").slice(1).join("/")}"`;
|
|
2658
|
+
return `\n<attachment name="${escapeXml(a.name)}" type="${a.mimeType}"${fileAttr}/>`;
|
|
2659
|
+
}).join("");
|
|
2660
|
+
return `<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${body}</message>`;
|
|
2661
|
+
}).join("\n")}\n</messages>`;
|
|
504
2662
|
}
|
|
505
2663
|
/** Format outbound text */
|
|
506
2664
|
function formatOutbound(rawText) {
|
|
@@ -510,7 +2668,9 @@ function formatOutbound(rawText) {
|
|
|
510
2668
|
function needsProcessing(group, messages) {
|
|
511
2669
|
if (group.folder === "main") return true;
|
|
512
2670
|
if (group.requiresTrigger === false) return true;
|
|
513
|
-
|
|
2671
|
+
const trigger = group.trigger || `@${config.get("assistantName")}`;
|
|
2672
|
+
const pattern = new RegExp(`^${trigger.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
2673
|
+
return messages.some((m) => pattern.test(m.content.trim()));
|
|
514
2674
|
}
|
|
515
2675
|
/**
|
|
516
2676
|
* Task scheduler — runs scheduled tasks (cron/interval/once) using the pi agent.
|
|
@@ -722,7 +2882,7 @@ var PiClawServer = class {
|
|
|
722
2882
|
folder: group.folder
|
|
723
2883
|
}, "Group unregistered");
|
|
724
2884
|
}
|
|
725
|
-
async sendBotMessage(chatJid, text) {
|
|
2885
|
+
async sendBotMessage(chatJid, text, meta) {
|
|
726
2886
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
727
2887
|
await storeChatMetadata(chatJid, now);
|
|
728
2888
|
await storeMessage({
|
|
@@ -733,8 +2893,10 @@ var PiClawServer = class {
|
|
|
733
2893
|
content: text,
|
|
734
2894
|
timestamp: now,
|
|
735
2895
|
is_from_me: true,
|
|
736
|
-
is_bot_message: true
|
|
2896
|
+
is_bot_message: true,
|
|
2897
|
+
meta
|
|
737
2898
|
});
|
|
2899
|
+
deviceBus.emitDeviceEvent("chats");
|
|
738
2900
|
for (const ch of this.channels) if (ch.ownsJid(chatJid)) {
|
|
739
2901
|
ch.sendMessage(chatJid, text).catch((err) => {
|
|
740
2902
|
logger.error({
|
|
@@ -749,7 +2911,7 @@ var PiClawServer = class {
|
|
|
749
2911
|
/** Full cleanup: kill agent, remove session/tasks/chat/group data */
|
|
750
2912
|
async cleanupChat(jid) {
|
|
751
2913
|
const group = this.registeredGroups[jid];
|
|
752
|
-
this.pi.kill(jid);
|
|
2914
|
+
await this.pi.kill(jid);
|
|
753
2915
|
if (group) {
|
|
754
2916
|
await removeSession(group.folder);
|
|
755
2917
|
const sessionDir = path$1.join(SESSIONS_DIR, group.folder);
|
|
@@ -785,8 +2947,18 @@ var PiClawServer = class {
|
|
|
785
2947
|
return new TelegramChannel({
|
|
786
2948
|
onMessage: async (jid, msg) => {
|
|
787
2949
|
await storeChatMetadata(jid, msg.timestamp, msg.sender_name, "telegram");
|
|
2950
|
+
const group = this.registeredGroups[jid];
|
|
2951
|
+
if (group) {
|
|
2952
|
+
const trimmed = msg.content.trim();
|
|
2953
|
+
if (tryPriorityCommand({
|
|
2954
|
+
chatJid: jid,
|
|
2955
|
+
group,
|
|
2956
|
+
server: this
|
|
2957
|
+
}, trimmed)) return;
|
|
2958
|
+
}
|
|
788
2959
|
await storeMessage(msg);
|
|
789
|
-
if (this.
|
|
2960
|
+
if (group) if (this.pi.sendMessage(jid, formatMessages([msg]))) await this.advanceCursor(jid, msg.timestamp);
|
|
2961
|
+
else this.queue.enqueueMessageCheck(jid);
|
|
790
2962
|
else await this.notifyUnregistered(jid);
|
|
791
2963
|
},
|
|
792
2964
|
onChatMetadata: async (jid, ts, name, channel, isGroup) => {
|
|
@@ -870,18 +3042,22 @@ var PiClawServer = class {
|
|
|
870
3042
|
await setRouterState("last_timestamp", this.lastTimestamp);
|
|
871
3043
|
await setRouterState("last_agent_timestamp", JSON.stringify(this.lastAgentTimestamp));
|
|
872
3044
|
}
|
|
3045
|
+
/** Advance the agent cursor for a chat — marks messages up to `timestamp` as processed. */
|
|
3046
|
+
async advanceCursor(chatJid, timestamp) {
|
|
3047
|
+
this.lastAgentTimestamp[chatJid] = timestamp;
|
|
3048
|
+
await this.saveState();
|
|
3049
|
+
}
|
|
873
3050
|
async handleCommands(chatJid, messages) {
|
|
874
3051
|
const group = this.registeredGroups[chatJid];
|
|
875
3052
|
if (!group) return messages;
|
|
876
3053
|
const remaining = [];
|
|
877
3054
|
for (const msg of messages) {
|
|
878
3055
|
const text = msg.content.trim();
|
|
879
|
-
|
|
3056
|
+
if (!await tryCommand({
|
|
880
3057
|
chatJid,
|
|
881
3058
|
group,
|
|
882
3059
|
server: this
|
|
883
|
-
};
|
|
884
|
-
if (!(tryBashCommand(ctx, text) || await tryCommand(ctx, text))) remaining.push(msg);
|
|
3060
|
+
}, text)) remaining.push(msg);
|
|
885
3061
|
}
|
|
886
3062
|
return remaining;
|
|
887
3063
|
}
|
|
@@ -900,6 +3076,18 @@ var PiClawServer = class {
|
|
|
900
3076
|
if (!needsProcessing(group, agentMessages)) return true;
|
|
901
3077
|
const prompt = formatMessages(agentMessages);
|
|
902
3078
|
const lastMessageTimestamp = missedMessages[missedMessages.length - 1].timestamp;
|
|
3079
|
+
const images = [];
|
|
3080
|
+
for (const msg of agentMessages) if (msg.meta?.attachments) {
|
|
3081
|
+
for (const att of msg.meta.attachments) if (isImageMimeType(att.mimeType)) try {
|
|
3082
|
+
images.push({
|
|
3083
|
+
type: "image",
|
|
3084
|
+
data: readAttachmentBase64(group.folder, att),
|
|
3085
|
+
mimeType: att.mimeType
|
|
3086
|
+
});
|
|
3087
|
+
} catch {
|
|
3088
|
+
logger.warn({ attachment: att.path }, "Failed to read attachment");
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
903
3091
|
if (!resolveGroupModel(group)) {
|
|
904
3092
|
logger.warn({ group: group.name }, "No model available, notifying user");
|
|
905
3093
|
await this.sendBotMessage(chatJid, "No AI model is currently available. Please configure a provider API key or log in via the admin panel.");
|
|
@@ -917,6 +3105,7 @@ var PiClawServer = class {
|
|
|
917
3105
|
try {
|
|
918
3106
|
await this.pi.run(group, {
|
|
919
3107
|
prompt,
|
|
3108
|
+
images: images.length ? images : void 0,
|
|
920
3109
|
sessionId: this.sessions[group.folder],
|
|
921
3110
|
groupFolder: group.folder,
|
|
922
3111
|
chatJid,
|
|
@@ -980,21 +3169,12 @@ var PiClawServer = class {
|
|
|
980
3169
|
for (const [chatJid, groupMessages] of byGroup) {
|
|
981
3170
|
const group = this.registeredGroups[chatJid];
|
|
982
3171
|
if (!group) continue;
|
|
983
|
-
for (const msg of groupMessages) {
|
|
984
|
-
const ctx = {
|
|
985
|
-
chatJid,
|
|
986
|
-
group,
|
|
987
|
-
server: this
|
|
988
|
-
};
|
|
989
|
-
tryBashCommand(ctx, msg.content.trim());
|
|
990
|
-
tryPriorityCommand(ctx, msg.content.trim());
|
|
991
|
-
}
|
|
992
3172
|
if (!needsProcessing(group, groupMessages)) continue;
|
|
993
3173
|
this.queue.enqueueMessageCheck(chatJid);
|
|
994
3174
|
}
|
|
995
3175
|
}
|
|
996
3176
|
} catch (err) {
|
|
997
|
-
logger.error({ err }, "Error in message loop");
|
|
3177
|
+
logger.error({ err: err?.stack || err?.message || err }, "Error in message loop");
|
|
998
3178
|
}
|
|
999
3179
|
await new Promise((resolve) => {
|
|
1000
3180
|
const timer = setTimeout(resolve, config.get("pollInterval"));
|
|
@@ -1028,4 +3208,4 @@ const startTime = globalThis.__piclaw_start_time__ || performance.now();
|
|
|
1028
3208
|
const server = new PiClawServer();
|
|
1029
3209
|
await server.start();
|
|
1030
3210
|
var server_default = () => {};
|
|
1031
|
-
export { server_default as n, startTime as r, server as t };
|
|
3211
|
+
export { commandDescriptions as a, authStorage as c, getSystemInfo as d, formatMessages as i, getFilteredModels as l, server_default as n, tryPriorityCommand as o, startTime as r, getPiSdkStatus as s, server as t, modelRegistry as u };
|