tau-all-agent 0.1.0
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/LICENSE +21 -0
- package/README.md +45 -0
- package/extensions/answer.ts +601 -0
- package/extensions/branch-term.ts +405 -0
- package/extensions/btw.ts +444 -0
- package/extensions/ghostty.ts +301 -0
- package/extensions/git-diff-stats.ts +277 -0
- package/extensions/git-pr-status.ts +286 -0
- package/extensions/insights.ts +2367 -0
- package/extensions/interlude.ts +144 -0
- package/extensions/loop.ts +529 -0
- package/extensions/memory.ts +1889 -0
- package/extensions/notify.ts +161 -0
- package/extensions/openai-fast.ts +227 -0
- package/extensions/openai-verbosity.ts +223 -0
- package/extensions/review.ts +4347 -0
- package/extensions/sandbox/index.ts +2578 -0
- package/extensions/telegram/README.md +81 -0
- package/extensions/telegram/daemon.mjs +1942 -0
- package/extensions/telegram/index.ts +1073 -0
- package/extensions/telegram/message-text.d.mts +1 -0
- package/extensions/telegram/message-text.mjs +16 -0
- package/extensions/usage/anthropic.ts +198 -0
- package/extensions/usage/github-copilot.ts +204 -0
- package/extensions/usage/google-gemini-cli.ts +232 -0
- package/extensions/usage/index.ts +2310 -0
- package/extensions/usage/minimax.ts +208 -0
- package/extensions/usage/openai-codex.ts +180 -0
- package/extensions/usage/openrouter.ts +168 -0
- package/extensions/usage/providers.ts +26 -0
- package/extensions/usage/shared.ts +156 -0
- package/extensions/usage/types.ts +53 -0
- package/extensions/usage/zai.ts +186 -0
- package/extensions/websearch/README.md +68 -0
- package/extensions/websearch/browser/chromium.ts +330 -0
- package/extensions/websearch/browser/discovery.ts +75 -0
- package/extensions/websearch/browser/firefox.ts +150 -0
- package/extensions/websearch/browser/sqlite.ts +38 -0
- package/extensions/websearch/config.ts +76 -0
- package/extensions/websearch/index.ts +312 -0
- package/extensions/websearch/normalize.ts +155 -0
- package/extensions/websearch/providers/anthropic.pi.ts +139 -0
- package/extensions/websearch/providers/gemini.browser.ts +185 -0
- package/extensions/websearch/providers/gemini.pi.ts +108 -0
- package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
- package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
- package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
- package/extensions/websearch/providers/pi-model.shared.ts +86 -0
- package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
- package/extensions/websearch/providers/shared.ts +76 -0
- package/extensions/websearch/types.ts +52 -0
- package/extensions/worktree.ts +2223 -0
- package/package.json +76 -0
- package/skills/browser-tools/SKILL.md +253 -0
- package/skills/browser-tools/scripts/browser-content.js +100 -0
- package/skills/browser-tools/scripts/browser-cookies.js +33 -0
- package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
- package/skills/browser-tools/scripts/browser-eval.js +75 -0
- package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
- package/skills/browser-tools/scripts/browser-nav.js +28 -0
- package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
- package/skills/browser-tools/scripts/browser-pick.js +133 -0
- package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
- package/skills/browser-tools/scripts/browser-start.js +281 -0
- package/skills/browser-tools/scripts/browser-watch.js +321 -0
- package/skills/browser-tools/scripts/utils.js +60 -0
- package/skills/git-clean-history/SKILL.md +45 -0
- package/skills/git-commit/SKILL.md +55 -0
- package/skills/homeassistant-ops/SKILL.md +184 -0
- package/skills/homeassistant-ops/references/api.md +64 -0
- package/skills/homeassistant-ops/references/id_conventions.md +71 -0
- package/skills/homeassistant-ops/references/playbook.md +150 -0
- package/skills/homeassistant-ops/scripts/ha_ops.js +2942 -0
- package/skills/openscad/SKILL.md +149 -0
- package/skills/openscad/examples/parametric_box.scad +92 -0
- package/skills/openscad/examples/phone_stand.scad +95 -0
- package/skills/openscad/scripts/common.sh +49 -0
- package/skills/openscad/scripts/export-stl.sh +56 -0
- package/skills/openscad/scripts/extract-params.sh +147 -0
- package/skills/openscad/scripts/multi-preview.sh +68 -0
- package/skills/openscad/scripts/preview.sh +74 -0
- package/skills/openscad/scripts/render-with-params.sh +91 -0
- package/skills/openscad/scripts/validate.sh +46 -0
- package/skills/oracle/SKILL.md +64 -0
- package/skills/oracle/scripts/oracle +249 -0
- package/skills/oracle/scripts/oracle-bundle +227 -0
- package/skills/sentry/SKILL.md +198 -0
- package/skills/sentry/lib/auth.js +99 -0
- package/skills/sentry/scripts/fetch-event.js +325 -0
- package/skills/sentry/scripts/fetch-issue.js +368 -0
- package/skills/sentry/scripts/list-issues.js +245 -0
- package/skills/sentry/scripts/search-events.js +291 -0
- package/skills/sentry/scripts/search-logs.js +234 -0
- package/skills/update-changelog/SKILL.md +135 -0
- package/skills/web-design/SKILL.md +117 -0
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BorderedLoader,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionContext,
|
|
5
|
+
type ExtensionCommandContext,
|
|
6
|
+
} from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import fsp from "node:fs/promises";
|
|
12
|
+
import { execFile, spawn } from "node:child_process";
|
|
13
|
+
import { extractTextFromMessage } from "./message-text.mjs";
|
|
14
|
+
|
|
15
|
+
const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
16
|
+
const RUN_DIR = path.join(AGENT_DIR, "run");
|
|
17
|
+
const SOCKET_PATH = path.join(RUN_DIR, "telegram.sock");
|
|
18
|
+
const CONFIG_DIR = path.join(AGENT_DIR, "telegram");
|
|
19
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
20
|
+
const TELEGRAM_BOT_TOKEN_ENV = "PI_TELEGRAM_BOT_TOKEN";
|
|
21
|
+
const TELEGRAM_KEYCHAIN_SERVICE = "pi.telegram";
|
|
22
|
+
const TELEGRAM_KEYCHAIN_ACCOUNT = "bot-token";
|
|
23
|
+
const AUTO_CONNECT_INTERVAL_MS = 3_000;
|
|
24
|
+
const COMPACTION_RELEASE_DELAY_MS = 500;
|
|
25
|
+
const COMPACTION_STALE_RESET_MS = 120_000;
|
|
26
|
+
|
|
27
|
+
type WindowSessionRef = {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
sessionFile?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type PendingInject = WindowSessionRef & {
|
|
33
|
+
id: string;
|
|
34
|
+
text: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type DaemonToClientMessage =
|
|
38
|
+
| { type: "registered"; sessionNo: number }
|
|
39
|
+
| { type: "pin"; code: string; expiresAt: number }
|
|
40
|
+
| { type: "paired"; chatId: number }
|
|
41
|
+
| { type: "error"; error: string }
|
|
42
|
+
| ({ type: "inject"; id: string; text: string } & WindowSessionRef)
|
|
43
|
+
| { type: "abort" };
|
|
44
|
+
|
|
45
|
+
type ClientToDaemonMessage =
|
|
46
|
+
| ({
|
|
47
|
+
type: "register";
|
|
48
|
+
windowId: string;
|
|
49
|
+
cwd: string;
|
|
50
|
+
sessionName?: string;
|
|
51
|
+
busy: boolean;
|
|
52
|
+
compacting: boolean;
|
|
53
|
+
} & WindowSessionRef)
|
|
54
|
+
| ({
|
|
55
|
+
type: "meta";
|
|
56
|
+
cwd: string;
|
|
57
|
+
sessionName?: string;
|
|
58
|
+
busy: boolean;
|
|
59
|
+
compacting: boolean;
|
|
60
|
+
} & WindowSessionRef)
|
|
61
|
+
| { type: "inject_result"; id: string; status: "accepted" | "rejected"; reason?: string }
|
|
62
|
+
| { type: "request_pin" }
|
|
63
|
+
| { type: "shutdown" }
|
|
64
|
+
| { type: "turn_end"; text: string };
|
|
65
|
+
|
|
66
|
+
type Config = {
|
|
67
|
+
botToken?: string;
|
|
68
|
+
pairedChatId?: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type PromptStatus = "completed" | "error";
|
|
72
|
+
type TelegramBotTokenSource = "env" | "keychain" | "config" | "missing";
|
|
73
|
+
|
|
74
|
+
async function withPromptSignal<T>(pi: ExtensionAPI, run: () => Promise<T>): Promise<T> {
|
|
75
|
+
pi.events.emit("ui:prompt_start", { source: "telegram" });
|
|
76
|
+
|
|
77
|
+
let status: PromptStatus = "completed";
|
|
78
|
+
try {
|
|
79
|
+
return await run();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
status = "error";
|
|
82
|
+
throw error;
|
|
83
|
+
} finally {
|
|
84
|
+
pi.events.emit("ui:prompt_end", { source: "telegram", status });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sleep(ms: number, signal?: AbortSignal) {
|
|
89
|
+
return new Promise<void>((resolve, reject) => {
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
cleanup();
|
|
92
|
+
resolve();
|
|
93
|
+
}, ms);
|
|
94
|
+
|
|
95
|
+
const onAbort = () => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
cleanup();
|
|
98
|
+
reject(new Error("Cancelled"));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const cleanup = () => {
|
|
102
|
+
signal?.removeEventListener("abort", onAbort);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (!signal) return;
|
|
106
|
+
if (signal.aborted) {
|
|
107
|
+
onAbort();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function throwIfAborted(signal?: AbortSignal) {
|
|
115
|
+
if (signal?.aborted) {
|
|
116
|
+
throw new Error("Cancelled");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runWithLoader<T>(
|
|
121
|
+
ctx: ExtensionContext,
|
|
122
|
+
message: string,
|
|
123
|
+
task: (signal: AbortSignal) => Promise<T>,
|
|
124
|
+
): Promise<{ cancelled: boolean; value?: T; error?: string }> {
|
|
125
|
+
if (!ctx.hasUI) {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
try {
|
|
128
|
+
const value = await task(controller.signal);
|
|
129
|
+
return { cancelled: false, value };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
cancelled: false,
|
|
133
|
+
error: error instanceof Error ? error.message : String(error),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = await ctx.ui.custom<{ cancelled: boolean; value?: T; error?: string }>(
|
|
139
|
+
(tui, theme, _kb, done) => {
|
|
140
|
+
const loader = new BorderedLoader(tui, theme, message);
|
|
141
|
+
let settled = false;
|
|
142
|
+
const finish = (value: { cancelled: boolean; value?: T; error?: string }) => {
|
|
143
|
+
if (settled) return;
|
|
144
|
+
settled = true;
|
|
145
|
+
done(value);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
loader.onAbort = () => finish({ cancelled: true });
|
|
149
|
+
|
|
150
|
+
task(loader.signal)
|
|
151
|
+
.then((value) => finish({ cancelled: false, value }))
|
|
152
|
+
.catch((error) => {
|
|
153
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
|
+
finish({ cancelled: false, error: errorMessage });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return loader;
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function loadConfig(): Promise<Config> {
|
|
165
|
+
try {
|
|
166
|
+
const raw = await fsp.readFile(CONFIG_PATH, "utf8");
|
|
167
|
+
return JSON.parse(raw) as Config;
|
|
168
|
+
} catch {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function removeDirectoryIfEmpty(dirPath: string): Promise<void> {
|
|
174
|
+
try {
|
|
175
|
+
const entries = await fsp.readdir(dirPath);
|
|
176
|
+
if (!entries.length) {
|
|
177
|
+
await fsp.rmdir(dirPath);
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// ignore
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function saveConfig(cfg: Config): Promise<void> {
|
|
185
|
+
const nextConfig: Config = {};
|
|
186
|
+
const botToken = cfg.botToken?.trim();
|
|
187
|
+
if (botToken) nextConfig.botToken = botToken;
|
|
188
|
+
if (cfg.pairedChatId !== undefined) nextConfig.pairedChatId = cfg.pairedChatId;
|
|
189
|
+
|
|
190
|
+
if (nextConfig.botToken === undefined && nextConfig.pairedChatId === undefined) {
|
|
191
|
+
await fsp.unlink(CONFIG_PATH).catch(() => undefined);
|
|
192
|
+
await removeDirectoryIfEmpty(CONFIG_DIR);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await fsp.mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
197
|
+
const tmp = CONFIG_PATH + ".tmp";
|
|
198
|
+
await fsp.writeFile(tmp, JSON.stringify(nextConfig, null, 2) + "\n", { mode: 0o600 });
|
|
199
|
+
await fsp.rename(tmp, CONFIG_PATH);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function canUseTelegramKeychain(): boolean {
|
|
203
|
+
return process.platform === "darwin";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runTelegramKeychainCommand(args: string[]): Promise<string> {
|
|
207
|
+
return await new Promise((resolve, reject) => {
|
|
208
|
+
execFile("security", args, { encoding: "utf8" }, (error, stdout) => {
|
|
209
|
+
if (error) {
|
|
210
|
+
reject(error);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
resolve(stdout.trim());
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readTelegramBotTokenFromKeychain(): Promise<string | null> {
|
|
219
|
+
if (!canUseTelegramKeychain()) return null;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
return (
|
|
223
|
+
(await runTelegramKeychainCommand([
|
|
224
|
+
"find-generic-password",
|
|
225
|
+
"-w",
|
|
226
|
+
"-a",
|
|
227
|
+
TELEGRAM_KEYCHAIN_ACCOUNT,
|
|
228
|
+
"-s",
|
|
229
|
+
TELEGRAM_KEYCHAIN_SERVICE,
|
|
230
|
+
])) || null
|
|
231
|
+
);
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function saveTelegramBotTokenToKeychain(token: string): Promise<void> {
|
|
238
|
+
if (!canUseTelegramKeychain()) {
|
|
239
|
+
throw new Error("macOS Keychain is not available for Telegram bot token storage.");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await runTelegramKeychainCommand([
|
|
244
|
+
"add-generic-password",
|
|
245
|
+
"-U",
|
|
246
|
+
"-w",
|
|
247
|
+
token,
|
|
248
|
+
"-a",
|
|
249
|
+
TELEGRAM_KEYCHAIN_ACCOUNT,
|
|
250
|
+
"-s",
|
|
251
|
+
TELEGRAM_KEYCHAIN_SERVICE,
|
|
252
|
+
]);
|
|
253
|
+
} catch {
|
|
254
|
+
throw new Error("Failed to save Telegram bot token to macOS Keychain.");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildPersistedConfig(config: Config, tokenSource: TelegramBotTokenSource): Config {
|
|
259
|
+
const nextConfig: Config = {};
|
|
260
|
+
if (tokenSource === "config" && config.botToken?.trim())
|
|
261
|
+
nextConfig.botToken = config.botToken.trim();
|
|
262
|
+
if (config.pairedChatId !== undefined) nextConfig.pairedChatId = config.pairedChatId;
|
|
263
|
+
return nextConfig;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function clearTelegramConfigBotToken(): Promise<void> {
|
|
267
|
+
const config = await loadConfig();
|
|
268
|
+
if (!config.botToken?.trim()) return;
|
|
269
|
+
delete config.botToken;
|
|
270
|
+
await saveConfig(config);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function resolveTelegramBotToken(): Promise<{
|
|
274
|
+
token: string | null;
|
|
275
|
+
source: TelegramBotTokenSource;
|
|
276
|
+
}> {
|
|
277
|
+
const envToken = process.env[TELEGRAM_BOT_TOKEN_ENV]?.trim();
|
|
278
|
+
if (envToken) return { token: envToken, source: "env" };
|
|
279
|
+
|
|
280
|
+
const keychainToken = await readTelegramBotTokenFromKeychain();
|
|
281
|
+
if (keychainToken) return { token: keychainToken, source: "keychain" };
|
|
282
|
+
|
|
283
|
+
const config = await loadConfig();
|
|
284
|
+
const configuredToken = config.botToken?.trim();
|
|
285
|
+
if (configuredToken) return { token: configuredToken, source: "config" };
|
|
286
|
+
|
|
287
|
+
return { token: null, source: "missing" };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function describeTelegramBotTokenSetup(): string {
|
|
291
|
+
if (canUseTelegramKeychain()) {
|
|
292
|
+
return `Set ${TELEGRAM_BOT_TOKEN_ENV}, store it in macOS Keychain via /telegram pair, or create ${CONFIG_PATH} with {"botToken": "..."}.`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return `Set ${TELEGRAM_BOT_TOKEN_ENV} or create ${CONFIG_PATH} with {"botToken": "..."}.`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseArgs(args: string | undefined): string[] {
|
|
299
|
+
if (!args) return [];
|
|
300
|
+
const trimmed = args.trim();
|
|
301
|
+
if (!trimmed) return [];
|
|
302
|
+
return trimmed.split(/\s+/g);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function getTelegramArgumentCompletions(
|
|
306
|
+
prefix: string,
|
|
307
|
+
): Array<{ value: string; label: string }> | null {
|
|
308
|
+
const trimmed = prefix.trim().toLowerCase();
|
|
309
|
+
if (trimmed.includes(" ")) return null;
|
|
310
|
+
|
|
311
|
+
const options = ["pair", "status", "unpair", "help"];
|
|
312
|
+
const matches = options.filter((option) => option.startsWith(trimmed));
|
|
313
|
+
if (!matches.length) return null;
|
|
314
|
+
|
|
315
|
+
return matches.map((option) => ({ value: option, label: option }));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getCurrentSessionRef(ctx: ExtensionContext): WindowSessionRef {
|
|
319
|
+
return {
|
|
320
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
321
|
+
sessionFile: ctx.sessionManager.getSessionFile() ?? undefined,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isPendingInjectForCurrentSession(
|
|
326
|
+
inject: WindowSessionRef,
|
|
327
|
+
ctx: ExtensionContext,
|
|
328
|
+
): boolean {
|
|
329
|
+
const current = getCurrentSessionRef(ctx);
|
|
330
|
+
if (inject.sessionId !== current.sessionId) return false;
|
|
331
|
+
return inject.sessionFile === current.sessionFile;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function describeInjectSessionMismatch(inject: WindowSessionRef, ctx: ExtensionContext): string {
|
|
335
|
+
const current = getCurrentSessionRef(ctx);
|
|
336
|
+
if (inject.sessionId === current.sessionId) {
|
|
337
|
+
return "Window session file changed before the Telegram message could be delivered.";
|
|
338
|
+
}
|
|
339
|
+
return "Window switched to a different Pi session before the Telegram message could be delivered.";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function jsonlWrite(socket: net.Socket, msg: ClientToDaemonMessage) {
|
|
343
|
+
socket.write(JSON.stringify(msg) + "\n");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function createJsonlReader(socket: net.Socket, onMessage: (msg: DaemonToClientMessage) => void) {
|
|
347
|
+
socket.setEncoding("utf8");
|
|
348
|
+
let buf = "";
|
|
349
|
+
socket.on("data", (data: string) => {
|
|
350
|
+
buf += data;
|
|
351
|
+
while (true) {
|
|
352
|
+
const idx = buf.indexOf("\n");
|
|
353
|
+
if (idx === -1) break;
|
|
354
|
+
const line = buf.slice(0, idx).trim();
|
|
355
|
+
buf = buf.slice(idx + 1);
|
|
356
|
+
if (!line) continue;
|
|
357
|
+
try {
|
|
358
|
+
const msg = JSON.parse(line);
|
|
359
|
+
if (msg && typeof msg.type === "string") onMessage(msg as DaemonToClientMessage);
|
|
360
|
+
} catch {
|
|
361
|
+
// ignore
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function canConnectSocket(): Promise<boolean> {
|
|
368
|
+
return await new Promise((resolve) => {
|
|
369
|
+
const s = net.connect(SOCKET_PATH);
|
|
370
|
+
s.once("connect", () => {
|
|
371
|
+
s.end();
|
|
372
|
+
resolve(true);
|
|
373
|
+
});
|
|
374
|
+
s.once("error", () => resolve(false));
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function ensureDaemonRunning(daemonPath: string, signal?: AbortSignal): Promise<void> {
|
|
379
|
+
throwIfAborted(signal);
|
|
380
|
+
await fsp.mkdir(RUN_DIR, { recursive: true, mode: 0o700 });
|
|
381
|
+
|
|
382
|
+
throwIfAborted(signal);
|
|
383
|
+
if (await canConnectSocket()) return;
|
|
384
|
+
|
|
385
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
386
|
+
detached: true,
|
|
387
|
+
stdio: "ignore",
|
|
388
|
+
env: {
|
|
389
|
+
...process.env,
|
|
390
|
+
PI_TELEGRAM_PI_EXECUTABLE: process.execPath,
|
|
391
|
+
PI_TELEGRAM_PI_ENTRYPOINT: process.argv[1] ?? "",
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
child.unref();
|
|
395
|
+
|
|
396
|
+
for (let i = 0; i < 40; i++) {
|
|
397
|
+
throwIfAborted(signal);
|
|
398
|
+
if (await canConnectSocket()) return;
|
|
399
|
+
await sleep(100, signal);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
throw new Error("Failed to start telegram daemon (socket not available)");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function sendEphemeral(msg: ClientToDaemonMessage): Promise<void> {
|
|
406
|
+
const socket = net.connect(SOCKET_PATH);
|
|
407
|
+
|
|
408
|
+
await new Promise<void>((resolve, reject) => {
|
|
409
|
+
const onError = (error: Error) => {
|
|
410
|
+
cleanup();
|
|
411
|
+
reject(error);
|
|
412
|
+
};
|
|
413
|
+
const onConnect = () => {
|
|
414
|
+
cleanup();
|
|
415
|
+
resolve();
|
|
416
|
+
};
|
|
417
|
+
const cleanup = () => {
|
|
418
|
+
socket.off("error", onError);
|
|
419
|
+
socket.off("connect", onConnect);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
socket.once("error", onError);
|
|
423
|
+
socket.once("connect", onConnect);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const payload = JSON.stringify(msg) + "\n";
|
|
427
|
+
|
|
428
|
+
await new Promise<void>((resolve, reject) => {
|
|
429
|
+
const onError = (error: Error) => {
|
|
430
|
+
socket.off("error", onError);
|
|
431
|
+
reject(error);
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
socket.once("error", onError);
|
|
435
|
+
socket.end(payload, () => {
|
|
436
|
+
socket.off("error", onError);
|
|
437
|
+
resolve();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export default function (pi: ExtensionAPI) {
|
|
443
|
+
if (process.env.PI_TELEGRAM_DISABLE === "1") return;
|
|
444
|
+
|
|
445
|
+
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
446
|
+
const daemonPath = path.join(extensionDir, "daemon.mjs");
|
|
447
|
+
|
|
448
|
+
const state = {
|
|
449
|
+
socket: null as net.Socket | null,
|
|
450
|
+
windowId: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
451
|
+
sessionNo: null as number | null,
|
|
452
|
+
busy: false,
|
|
453
|
+
compacting: false,
|
|
454
|
+
pendingInjectedTexts: [] as PendingInject[],
|
|
455
|
+
flushInjectedTextsPromise: null as Promise<void> | null,
|
|
456
|
+
pendingInjectedFlushTimer: null as ReturnType<typeof setTimeout> | null,
|
|
457
|
+
compactionResetTimer: null as ReturnType<typeof setTimeout> | null,
|
|
458
|
+
lastCtx: null as ExtensionContext | null,
|
|
459
|
+
connectPromise: null as Promise<void> | null,
|
|
460
|
+
autoConnectTimer: null as ReturnType<typeof setInterval> | null,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const daemonMessageHandlers = new Set<(msg: DaemonToClientMessage) => void>();
|
|
464
|
+
|
|
465
|
+
function clearPendingInjectedFlushTimer() {
|
|
466
|
+
if (!state.pendingInjectedFlushTimer) return;
|
|
467
|
+
clearTimeout(state.pendingInjectedFlushTimer);
|
|
468
|
+
state.pendingInjectedFlushTimer = null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function schedulePendingInjectedFlush(delayMs = 0) {
|
|
472
|
+
if (state.pendingInjectedFlushTimer) return;
|
|
473
|
+
state.pendingInjectedFlushTimer = setTimeout(() => {
|
|
474
|
+
state.pendingInjectedFlushTimer = null;
|
|
475
|
+
void flushPendingInjectedTexts();
|
|
476
|
+
}, delayMs);
|
|
477
|
+
state.pendingInjectedFlushTimer.unref?.();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function clearCompactionResetTimer() {
|
|
481
|
+
if (!state.compactionResetTimer) return;
|
|
482
|
+
clearTimeout(state.compactionResetTimer);
|
|
483
|
+
state.compactionResetTimer = null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function applyCompactingState(compacting: boolean, ctx?: ExtensionContext | null) {
|
|
487
|
+
state.compacting = compacting;
|
|
488
|
+
|
|
489
|
+
if (compacting) {
|
|
490
|
+
clearCompactionResetTimer();
|
|
491
|
+
state.compactionResetTimer = setTimeout(() => {
|
|
492
|
+
state.compactionResetTimer = null;
|
|
493
|
+
applyCompactingState(false, state.lastCtx);
|
|
494
|
+
}, COMPACTION_STALE_RESET_MS);
|
|
495
|
+
state.compactionResetTimer.unref?.();
|
|
496
|
+
} else {
|
|
497
|
+
clearCompactionResetTimer();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const currentCtx = ctx ?? state.lastCtx;
|
|
501
|
+
if (currentCtx) {
|
|
502
|
+
if (isSocketConnected()) {
|
|
503
|
+
updateMeta(currentCtx);
|
|
504
|
+
} else {
|
|
505
|
+
void tryAutoConnect();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!compacting) {
|
|
510
|
+
void flushPendingInjectedTexts();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function flushPendingInjectedTexts(): Promise<void> {
|
|
515
|
+
if (state.flushInjectedTextsPromise) {
|
|
516
|
+
await state.flushInjectedTextsPromise;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const flushPromise = (async () => {
|
|
521
|
+
while (true) {
|
|
522
|
+
const ctx = state.lastCtx;
|
|
523
|
+
const inject = state.pendingInjectedTexts[0];
|
|
524
|
+
if (!ctx || !inject) return;
|
|
525
|
+
if (state.compacting) return;
|
|
526
|
+
|
|
527
|
+
if (!isPendingInjectForCurrentSession(inject, ctx)) {
|
|
528
|
+
state.pendingInjectedTexts.shift();
|
|
529
|
+
sendInjectResult(inject.id, "rejected", describeInjectSessionMismatch(inject, ctx));
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (ctx.isIdle() && ctx.hasPendingMessages()) {
|
|
534
|
+
schedulePendingInjectedFlush(500);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
if (ctx.isIdle()) {
|
|
540
|
+
pi.sendUserMessage(inject.text);
|
|
541
|
+
state.pendingInjectedTexts.shift();
|
|
542
|
+
sendInjectResult(inject.id, "accepted");
|
|
543
|
+
if (state.pendingInjectedTexts.length > 0) {
|
|
544
|
+
schedulePendingInjectedFlush(500);
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
pi.sendUserMessage(inject.text, { deliverAs: "followUp" });
|
|
550
|
+
state.pendingInjectedTexts.shift();
|
|
551
|
+
sendInjectResult(inject.id, "accepted");
|
|
552
|
+
} catch {
|
|
553
|
+
schedulePendingInjectedFlush(500);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
})();
|
|
558
|
+
|
|
559
|
+
state.flushInjectedTextsPromise = flushPromise;
|
|
560
|
+
try {
|
|
561
|
+
await flushPromise;
|
|
562
|
+
} finally {
|
|
563
|
+
if (state.flushInjectedTextsPromise === flushPromise) {
|
|
564
|
+
state.flushInjectedTextsPromise = null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function isSocketConnected() {
|
|
570
|
+
return !!(state.socket && !state.socket.destroyed);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function clearUI(ctx?: ExtensionContext | null) {
|
|
574
|
+
if (!ctx?.hasUI) return;
|
|
575
|
+
ctx.ui.setStatus("telegram", undefined);
|
|
576
|
+
ctx.ui.setWidget("telegram", undefined);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function connectedStatusText(ctx: ExtensionContext, sessionNo: number): string {
|
|
580
|
+
const text = `telegram (session ${sessionNo})`;
|
|
581
|
+
return ctx.hasUI ? ctx.ui.theme.fg("dim", text) : text;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function disconnect(restartAutoConnect = true) {
|
|
585
|
+
const socket = state.socket;
|
|
586
|
+
state.socket = null;
|
|
587
|
+
state.sessionNo = null;
|
|
588
|
+
clearUI(state.lastCtx);
|
|
589
|
+
|
|
590
|
+
if (restartAutoConnect) {
|
|
591
|
+
startAutoConnectLoop();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (!socket || socket.destroyed) return;
|
|
595
|
+
try {
|
|
596
|
+
socket.end();
|
|
597
|
+
} catch {}
|
|
598
|
+
try {
|
|
599
|
+
socket.destroy();
|
|
600
|
+
} catch {}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function send(msg: ClientToDaemonMessage) {
|
|
604
|
+
if (!state.socket || state.socket.destroyed) return;
|
|
605
|
+
try {
|
|
606
|
+
jsonlWrite(state.socket, msg);
|
|
607
|
+
} catch {
|
|
608
|
+
// ignore
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function sendInjectResult(id: string, status: "accepted" | "rejected", reason?: string) {
|
|
613
|
+
send({ type: "inject_result", id, status, reason });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function updateMeta(ctx: ExtensionContext) {
|
|
617
|
+
const sessionName = pi.getSessionName() ?? undefined;
|
|
618
|
+
send({
|
|
619
|
+
type: "meta",
|
|
620
|
+
cwd: ctx.cwd,
|
|
621
|
+
sessionName,
|
|
622
|
+
busy: state.busy,
|
|
623
|
+
compacting: state.compacting,
|
|
624
|
+
...getCurrentSessionRef(ctx),
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function connectPersistent(
|
|
629
|
+
ctx: ExtensionContext,
|
|
630
|
+
options: { signal?: AbortSignal; ensureDaemon?: boolean } = {},
|
|
631
|
+
): Promise<void> {
|
|
632
|
+
if (isSocketConnected()) return;
|
|
633
|
+
|
|
634
|
+
if (state.connectPromise) {
|
|
635
|
+
try {
|
|
636
|
+
await state.connectPromise;
|
|
637
|
+
} catch {
|
|
638
|
+
// ignore; we may retry below
|
|
639
|
+
}
|
|
640
|
+
if (isSocketConnected()) return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const { signal, ensureDaemon = true } = options;
|
|
644
|
+
|
|
645
|
+
const connectPromise = (async () => {
|
|
646
|
+
if (ensureDaemon) {
|
|
647
|
+
await ensureDaemonRunning(daemonPath, signal);
|
|
648
|
+
throwIfAborted(signal);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const socket = net.connect(SOCKET_PATH);
|
|
652
|
+
|
|
653
|
+
await new Promise<void>((resolve, reject) => {
|
|
654
|
+
const onConnect = () => {
|
|
655
|
+
cleanup();
|
|
656
|
+
resolve();
|
|
657
|
+
};
|
|
658
|
+
const onError = (error: Error) => {
|
|
659
|
+
cleanup();
|
|
660
|
+
reject(error);
|
|
661
|
+
};
|
|
662
|
+
const onAbort = () => {
|
|
663
|
+
cleanup();
|
|
664
|
+
try {
|
|
665
|
+
socket.destroy();
|
|
666
|
+
} catch {}
|
|
667
|
+
reject(new Error("Cancelled"));
|
|
668
|
+
};
|
|
669
|
+
const cleanup = () => {
|
|
670
|
+
socket.off("connect", onConnect);
|
|
671
|
+
socket.off("error", onError);
|
|
672
|
+
signal?.removeEventListener("abort", onAbort);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
socket.once("connect", onConnect);
|
|
676
|
+
socket.once("error", onError);
|
|
677
|
+
|
|
678
|
+
if (!signal) return;
|
|
679
|
+
if (signal.aborted) {
|
|
680
|
+
onAbort();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
state.socket = socket;
|
|
687
|
+
createJsonlReader(socket, handleDaemonMessage);
|
|
688
|
+
|
|
689
|
+
const onSocketGone = () => {
|
|
690
|
+
if (state.socket === socket) {
|
|
691
|
+
state.socket = null;
|
|
692
|
+
state.sessionNo = null;
|
|
693
|
+
clearUI(state.lastCtx);
|
|
694
|
+
startAutoConnectLoop();
|
|
695
|
+
void tryAutoConnect();
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
socket.once("close", onSocketGone);
|
|
700
|
+
socket.once("error", onSocketGone);
|
|
701
|
+
|
|
702
|
+
jsonlWrite(socket, {
|
|
703
|
+
type: "register",
|
|
704
|
+
windowId: state.windowId,
|
|
705
|
+
cwd: ctx.cwd,
|
|
706
|
+
sessionName: pi.getSessionName() ?? undefined,
|
|
707
|
+
busy: state.busy,
|
|
708
|
+
compacting: state.compacting,
|
|
709
|
+
...getCurrentSessionRef(ctx),
|
|
710
|
+
});
|
|
711
|
+
})();
|
|
712
|
+
|
|
713
|
+
state.connectPromise = connectPromise;
|
|
714
|
+
try {
|
|
715
|
+
await connectPromise;
|
|
716
|
+
} finally {
|
|
717
|
+
if (state.connectPromise === connectPromise) {
|
|
718
|
+
state.connectPromise = null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function tryAutoConnect(): Promise<void> {
|
|
724
|
+
const ctx = state.lastCtx;
|
|
725
|
+
if (!ctx) return;
|
|
726
|
+
if (isSocketConnected()) return;
|
|
727
|
+
if (state.connectPromise) return;
|
|
728
|
+
|
|
729
|
+
const cfg = await loadConfig();
|
|
730
|
+
const tokenInfo = await resolveTelegramBotToken();
|
|
731
|
+
if (!tokenInfo.token || cfg.pairedChatId === undefined) return;
|
|
732
|
+
|
|
733
|
+
const daemonUp = await canConnectSocket();
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
await connectPersistent(ctx, { ensureDaemon: !daemonUp });
|
|
737
|
+
updateMeta(ctx);
|
|
738
|
+
} catch {
|
|
739
|
+
// ignore; next loop tick will retry
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function startAutoConnectLoop() {
|
|
744
|
+
if (state.autoConnectTimer) return;
|
|
745
|
+
state.autoConnectTimer = setInterval(() => {
|
|
746
|
+
void tryAutoConnect();
|
|
747
|
+
}, AUTO_CONNECT_INTERVAL_MS);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function stopAutoConnectLoop() {
|
|
751
|
+
if (!state.autoConnectTimer) return;
|
|
752
|
+
clearInterval(state.autoConnectTimer);
|
|
753
|
+
state.autoConnectTimer = null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async function requestPin(
|
|
757
|
+
signal?: AbortSignal,
|
|
758
|
+
): Promise<{ code: string; expiresAt: number } | null> {
|
|
759
|
+
if (!state.socket || state.socket.destroyed) return null;
|
|
760
|
+
|
|
761
|
+
return await new Promise((resolve) => {
|
|
762
|
+
let done = false;
|
|
763
|
+
const timeout = setTimeout(() => {
|
|
764
|
+
finish(null);
|
|
765
|
+
}, 10_000);
|
|
766
|
+
|
|
767
|
+
const finish = (value: { code: string; expiresAt: number } | null) => {
|
|
768
|
+
if (done) return;
|
|
769
|
+
done = true;
|
|
770
|
+
clearTimeout(timeout);
|
|
771
|
+
daemonMessageHandlers.delete(handler);
|
|
772
|
+
signal?.removeEventListener("abort", onAbort);
|
|
773
|
+
resolve(value);
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const handler = (msg: DaemonToClientMessage) => {
|
|
777
|
+
if (msg.type === "pin") {
|
|
778
|
+
finish({ code: msg.code, expiresAt: msg.expiresAt });
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (msg.type === "error") {
|
|
782
|
+
finish(null);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const onAbort = () => {
|
|
787
|
+
finish(null);
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
if (signal) {
|
|
791
|
+
if (signal.aborted) {
|
|
792
|
+
finish(null);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
daemonMessageHandlers.add(handler);
|
|
799
|
+
send({ type: "request_pin" });
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function handleDaemonMessage(msg: DaemonToClientMessage) {
|
|
804
|
+
for (const h of daemonMessageHandlers) {
|
|
805
|
+
try {
|
|
806
|
+
h(msg);
|
|
807
|
+
} catch {
|
|
808
|
+
// ignore
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (msg.type === "registered") {
|
|
813
|
+
state.sessionNo = msg.sessionNo;
|
|
814
|
+
stopAutoConnectLoop();
|
|
815
|
+
if (state.lastCtx?.hasUI) {
|
|
816
|
+
state.lastCtx.ui.setStatus("telegram", connectedStatusText(state.lastCtx, msg.sessionNo));
|
|
817
|
+
}
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (msg.type === "paired") {
|
|
822
|
+
if (state.lastCtx?.hasUI) {
|
|
823
|
+
state.lastCtx.ui.setWidget("telegram", undefined);
|
|
824
|
+
}
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (msg.type === "inject") {
|
|
829
|
+
const text = msg.text;
|
|
830
|
+
if (!text) return;
|
|
831
|
+
state.pendingInjectedTexts.push({
|
|
832
|
+
id: msg.id,
|
|
833
|
+
text,
|
|
834
|
+
sessionId: msg.sessionId,
|
|
835
|
+
sessionFile: msg.sessionFile,
|
|
836
|
+
});
|
|
837
|
+
void flushPendingInjectedTexts();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (msg.type === "abort") {
|
|
842
|
+
const ctx = state.lastCtx;
|
|
843
|
+
if (!ctx) return;
|
|
844
|
+
ctx.abort();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
850
|
+
state.lastCtx = ctx;
|
|
851
|
+
startAutoConnectLoop();
|
|
852
|
+
if (isSocketConnected()) {
|
|
853
|
+
updateMeta(ctx);
|
|
854
|
+
} else {
|
|
855
|
+
void tryAutoConnect();
|
|
856
|
+
}
|
|
857
|
+
void flushPendingInjectedTexts();
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
pi.on("input", async (event) => {
|
|
861
|
+
if (event.source !== "extension" && state.pendingInjectedTexts.length > 0) {
|
|
862
|
+
state.pendingInjectedTexts = [];
|
|
863
|
+
clearPendingInjectedFlushTimer();
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
868
|
+
if (state.compacting) {
|
|
869
|
+
applyCompactingState(false, ctx);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
state.busy = true;
|
|
873
|
+
if (isSocketConnected()) {
|
|
874
|
+
updateMeta(ctx);
|
|
875
|
+
void flushPendingInjectedTexts();
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
void tryAutoConnect();
|
|
879
|
+
void flushPendingInjectedTexts();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
883
|
+
state.busy = false;
|
|
884
|
+
if (isSocketConnected()) {
|
|
885
|
+
updateMeta(ctx);
|
|
886
|
+
void flushPendingInjectedTexts();
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
void tryAutoConnect();
|
|
890
|
+
void flushPendingInjectedTexts();
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
894
|
+
applyCompactingState(true, ctx);
|
|
895
|
+
event.signal.addEventListener(
|
|
896
|
+
"abort",
|
|
897
|
+
() => applyCompactingState(false, state.lastCtx ?? ctx),
|
|
898
|
+
{ once: true },
|
|
899
|
+
);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
903
|
+
clearCompactionResetTimer();
|
|
904
|
+
state.compactionResetTimer = setTimeout(() => {
|
|
905
|
+
state.compactionResetTimer = null;
|
|
906
|
+
applyCompactingState(false, state.lastCtx ?? ctx);
|
|
907
|
+
}, COMPACTION_RELEASE_DELAY_MS);
|
|
908
|
+
state.compactionResetTimer.unref?.();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
pi.on("turn_end", async (event: any) => {
|
|
912
|
+
if (!isSocketConnected()) return;
|
|
913
|
+
const text = extractTextFromMessage(event.message);
|
|
914
|
+
if (!text) return;
|
|
915
|
+
send({ type: "turn_end", text });
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
919
|
+
stopAutoConnectLoop();
|
|
920
|
+
clearPendingInjectedFlushTimer();
|
|
921
|
+
clearCompactionResetTimer();
|
|
922
|
+
state.busy = false;
|
|
923
|
+
state.compacting = false;
|
|
924
|
+
state.pendingInjectedTexts = [];
|
|
925
|
+
disconnect(false);
|
|
926
|
+
state.lastCtx = null;
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
pi.registerCommand("telegram", {
|
|
930
|
+
description: "Telegram bridge: /telegram pair | status | unpair",
|
|
931
|
+
getArgumentCompletions: getTelegramArgumentCompletions,
|
|
932
|
+
handler: async (args, ctx: ExtensionCommandContext) => {
|
|
933
|
+
const [sub] = parseArgs(args);
|
|
934
|
+
|
|
935
|
+
const notify = (text: string, level: "info" | "warning" | "error" = "info") => {
|
|
936
|
+
if (ctx.hasUI) ctx.ui.notify(text, level);
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
if (!sub || sub === "help") {
|
|
940
|
+
notify("Usage: /telegram pair | status | unpair", "info");
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (sub === "status") {
|
|
945
|
+
const cfg = await loadConfig();
|
|
946
|
+
const tokenInfo = await resolveTelegramBotToken();
|
|
947
|
+
const tokenState = tokenInfo.token ? tokenInfo.source : "missing";
|
|
948
|
+
const paired = cfg.pairedChatId !== undefined ? `paired (${cfg.pairedChatId})` : "unpaired";
|
|
949
|
+
const daemonUp = await canConnectSocket();
|
|
950
|
+
|
|
951
|
+
const lines = [
|
|
952
|
+
`Token: ${tokenState}`,
|
|
953
|
+
`Pairing: ${paired}`,
|
|
954
|
+
`Daemon: ${daemonUp ? "running" : "not running"}`,
|
|
955
|
+
`This window: ${isSocketConnected() && state.sessionNo !== null ? `connected (session ${state.sessionNo})` : "not connected"}`,
|
|
956
|
+
];
|
|
957
|
+
notify(lines.join("\n"), "info");
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (sub === "unpair") {
|
|
962
|
+
const cfg = await loadConfig();
|
|
963
|
+
const tokenInfo = await resolveTelegramBotToken();
|
|
964
|
+
if (cfg.pairedChatId !== undefined) {
|
|
965
|
+
delete cfg.pairedChatId;
|
|
966
|
+
await saveConfig(buildPersistedConfig(cfg, tokenInfo.source));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (await canConnectSocket()) {
|
|
970
|
+
try {
|
|
971
|
+
await sendEphemeral({ type: "shutdown" });
|
|
972
|
+
} catch {
|
|
973
|
+
// ignore
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
disconnect();
|
|
978
|
+
|
|
979
|
+
notify(
|
|
980
|
+
"Unpaired Telegram and disconnected all sessions. Run /telegram pair to pair again.",
|
|
981
|
+
"info",
|
|
982
|
+
);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (sub === "pair") {
|
|
987
|
+
const cfg = await loadConfig();
|
|
988
|
+
const tokenInfo = await resolveTelegramBotToken();
|
|
989
|
+
if (!tokenInfo.token) {
|
|
990
|
+
if (!ctx.hasUI) {
|
|
991
|
+
throw new Error(`Missing bot token. ${describeTelegramBotTokenSetup()}`);
|
|
992
|
+
}
|
|
993
|
+
const promptLocation = canUseTelegramKeychain()
|
|
994
|
+
? "saved to macOS Keychain"
|
|
995
|
+
: `stored in ${CONFIG_PATH}`;
|
|
996
|
+
const token = await withPromptSignal(pi, () =>
|
|
997
|
+
ctx.ui.input("Telegram bot token", `Paste the bot token (${promptLocation})`),
|
|
998
|
+
);
|
|
999
|
+
if (!token) {
|
|
1000
|
+
notify("Cancelled.", "info");
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (canUseTelegramKeychain()) {
|
|
1004
|
+
await saveTelegramBotTokenToKeychain(token.trim());
|
|
1005
|
+
await clearTelegramConfigBotToken();
|
|
1006
|
+
} else {
|
|
1007
|
+
await saveConfig({ ...cfg, botToken: token.trim() });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const connectResult = await runWithLoader(
|
|
1012
|
+
ctx,
|
|
1013
|
+
"Connecting to Telegram daemon...",
|
|
1014
|
+
(signal) => connectPersistent(ctx, { signal, ensureDaemon: true }),
|
|
1015
|
+
);
|
|
1016
|
+
if (connectResult.cancelled) {
|
|
1017
|
+
notify("Cancelled.", "info");
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (connectResult.error) {
|
|
1021
|
+
notify(`Failed to connect: ${connectResult.error}`, "error");
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
updateMeta(ctx);
|
|
1026
|
+
if (ctx.hasUI && state.sessionNo !== null) {
|
|
1027
|
+
ctx.ui.setStatus("telegram", connectedStatusText(ctx, state.sessionNo));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const freshCfg = await loadConfig();
|
|
1031
|
+
if (!freshCfg.pairedChatId) {
|
|
1032
|
+
const pinResult = await runWithLoader(
|
|
1033
|
+
ctx,
|
|
1034
|
+
"Requesting Telegram pairing PIN...",
|
|
1035
|
+
(signal) => requestPin(signal),
|
|
1036
|
+
);
|
|
1037
|
+
if (pinResult.cancelled) {
|
|
1038
|
+
notify("Cancelled.", "info");
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (pinResult.error) {
|
|
1042
|
+
notify(`Failed to request PIN: ${pinResult.error}`, "error");
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const pin = pinResult.value;
|
|
1047
|
+
if (!pin) {
|
|
1048
|
+
notify("Failed to request PIN from daemon.", "error");
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (ctx.hasUI) {
|
|
1053
|
+
ctx.ui.notify(`Send this in Telegram: /pin ${pin.code} (valid 60s)`, "info");
|
|
1054
|
+
ctx.ui.setWidget("telegram", [
|
|
1055
|
+
`Telegram pairing: send /pin ${pin.code} (valid 60s)`,
|
|
1056
|
+
"All open pi windows will appear in Telegram /sessions as [window].",
|
|
1057
|
+
"Use /session N in Telegram to switch sessions.",
|
|
1058
|
+
]);
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (ctx.hasUI) {
|
|
1064
|
+
ctx.ui.setWidget("telegram", undefined);
|
|
1065
|
+
}
|
|
1066
|
+
notify("Paired. Use Telegram /sessions to access all sessions.", "info");
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
notify(`Unknown subcommand: ${sub}. Use /telegram help`, "error");
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
}
|