pi-xmpp 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.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * XMPP extension slash commands
3
+ * Zones: pi agent commands, xmpp specific
4
+ * Owns /xmpp slash commands for connection management, status, and control
5
+ */
6
+
7
+ import type { ExtensionAPI } from "./pi.ts";
8
+ import type { XmppClientInstance, XmppConnectionStatus } from "./xmpp-api.ts";
9
+ import type { XmppConfig, XmppConfigStore } from "./config.ts";
10
+ import type { XmppBridgeRuntime } from "./runtime.ts";
11
+
12
+ export interface XmppExtensionCommandContext {
13
+ args: string[];
14
+ rawArgs: string;
15
+ }
16
+
17
+ export interface XmppExtensionCommandRegistration {
18
+ name: string;
19
+ description: string;
20
+ handler: (ctx: XmppExtensionCommandContext) => Promise<void>;
21
+ }
22
+
23
+ const COMMAND_REGISTRY_KEY = "__piXmppCommands__";
24
+
25
+ /**
26
+ * Register an extension slash command.
27
+ */
28
+ export function registerXmppCommand(
29
+ registration: XmppExtensionCommandRegistration,
30
+ ): void {
31
+ const registry = getCommandRegistry();
32
+ registry.push(registration);
33
+ }
34
+
35
+ function getCommandRegistry(): XmppExtensionCommandRegistration[] {
36
+ const globals = globalThis as Record<string, unknown>;
37
+ if (!globals[COMMAND_REGISTRY_KEY]) {
38
+ globals[COMMAND_REGISTRY_KEY] = [];
39
+ }
40
+ return globals[COMMAND_REGISTRY_KEY] as XmppExtensionCommandRegistration[];
41
+ }
42
+
43
+ export function createXmppSlashCommands(deps: {
44
+ client: XmppClientInstance;
45
+ configStore: XmppConfigStore;
46
+ runtime: XmppBridgeRuntime;
47
+ getConnectionStatus: () => string;
48
+ sendMessageToActiveTurn: (body: string) => Promise<void>;
49
+ updateStatus: () => void;
50
+ getAllowedJid: () => string | undefined;
51
+ }) {
52
+ return [
53
+ {
54
+ name: "xmpp-connect",
55
+ description:
56
+ "Connect the XMPP client to your server. Provide --jid, --password, and --service options.",
57
+ handler: async (ctx: XmppExtensionCommandContext) => {
58
+ const args = parseArgs(ctx.rawArgs);
59
+ const jid = args.jid ?? args.J;
60
+ const password = args.password ?? args.p;
61
+ const service = args.service ?? args.s;
62
+ const domain = args.domain ?? args.d;
63
+
64
+ if (!jid || !password) {
65
+ await deps.sendMessageToActiveTurn(
66
+ "Usage: /xmpp-connect --jid user@domain.tld --password <your_password> [--service xmpp://server.tld] [--domain domain.tld]",
67
+ );
68
+ return;
69
+ }
70
+
71
+ const config = deps.configStore.get();
72
+ config.jid = jid;
73
+ config.password = password;
74
+ config.service = service || config.service;
75
+ config.domain = domain || config.domain;
76
+ deps.configStore.set(config);
77
+ await deps.configStore.persist();
78
+
79
+ try {
80
+ await deps.client.connect(config);
81
+ deps.updateStatus();
82
+ await deps.sendMessageToActiveTurn(
83
+ `✅ Connected as ${jid}`,
84
+ );
85
+ } catch (error) {
86
+ await deps.sendMessageToActiveTurn(
87
+ `❌ Connection failed: ${error instanceof Error ? error.message : String(error)}`,
88
+ );
89
+ }
90
+ },
91
+ },
92
+ {
93
+ name: "xmpp-disconnect",
94
+ description: "Disconnect the XMPP client.",
95
+ handler: async () => {
96
+ await deps.client.disconnect();
97
+ deps.updateStatus();
98
+ await deps.sendMessageToActiveTurn("🔌 Disconnected from XMPP server.");
99
+ },
100
+ },
101
+ {
102
+ name: "xmpp-status",
103
+ description: "Show XMPP connection status and configuration info.",
104
+ handler: async () => {
105
+ const config = deps.configStore.get();
106
+ const status = deps.client.status;
107
+ const jid = deps.client.jid;
108
+ const allowedJid = deps.getAllowedJid();
109
+
110
+ const lines = [
111
+ `**XMPP Bridge Status**`,
112
+ ``,
113
+ `**Connection:** ${status}`,
114
+ `**JID:** ${jid ?? "not connected"}`,
115
+ `**Configured JID:** ${config.jid ?? "not configured"}`,
116
+ `**Service:** ${config.service ?? "auto"}`,
117
+ `**Allowed JID:** ${allowedJid ?? "not paired"}`,
118
+ `**Auto-reconnect:** ${config.autoReconnect ?? true}`,
119
+ ];
120
+
121
+ if (config.autoJoinRooms?.length) {
122
+ lines.push(`**Auto-join rooms:** ${config.autoJoinRooms.join(", ")}`);
123
+ }
124
+
125
+ await deps.sendMessageToActiveTurn(lines.join("\n"));
126
+ },
127
+ },
128
+ {
129
+ name: "xmpp-join",
130
+ description:
131
+ "Join a MUC room. Usage: /xmpp-join --room room@conference.tld --nick your_nick",
132
+ handler: async (ctx: XmppExtensionCommandContext) => {
133
+ const args = parseArgs(ctx.rawArgs);
134
+ const room = args.room ?? args.r;
135
+ const nick = args.nick ?? args.n;
136
+
137
+ if (!room) {
138
+ await deps.sendMessageToActiveTurn(
139
+ "Usage: /xmpp-join --room room@conference.tld [--nick your_nick]",
140
+ );
141
+ return;
142
+ }
143
+
144
+ const nickname = nick ?? deps.client.jid?.split("@")[0] ?? "pi";
145
+ deps.client.joinRoom(room, nickname);
146
+
147
+ const config = deps.configStore.get();
148
+ const rooms = config.autoJoinRooms ?? [];
149
+ if (!rooms.includes(room)) {
150
+ rooms.push(room);
151
+ config.autoJoinRooms = rooms;
152
+ deps.configStore.set(config);
153
+ await deps.configStore.persist();
154
+ }
155
+
156
+ await deps.sendMessageToActiveTurn(
157
+ `🚪 Joined room ${room} as ${nickname}`,
158
+ );
159
+ },
160
+ },
161
+ {
162
+ name: "xmpp-leave",
163
+ description:
164
+ "Leave a MUC room. Usage: /xmpp-leave --room room@conference.tld",
165
+ handler: async (ctx: XmppExtensionCommandContext) => {
166
+ const args = parseArgs(ctx.rawArgs);
167
+ const room = args.room ?? args.r;
168
+
169
+ if (!room) {
170
+ await deps.sendMessageToActiveTurn(
171
+ "Usage: /xmpp-leave --room room@conference.tld",
172
+ );
173
+ return;
174
+ }
175
+
176
+ deps.client.leaveRoom(room);
177
+
178
+ const config = deps.configStore.get();
179
+ if (config.autoJoinRooms) {
180
+ config.autoJoinRooms = config.autoJoinRooms.filter(
181
+ (r) => r !== room,
182
+ );
183
+ deps.configStore.set(config);
184
+ await deps.configStore.persist();
185
+ }
186
+
187
+ await deps.sendMessageToActiveTurn(`🚪 Left room ${room}`);
188
+ },
189
+ },
190
+ {
191
+ name: "xmpp-set-presence",
192
+ description:
193
+ "Set your presence. Usage: /xmpp-set-presence [--show chat|away|dnd|xa] [--status message]",
194
+ handler: async (ctx: XmppExtensionCommandContext) => {
195
+ const args = parseArgs(ctx.rawArgs);
196
+ const show = args.show ?? args.s;
197
+ const status = args.status ?? args.m;
198
+
199
+ deps.client.sendPresence({
200
+ show: show || undefined,
201
+ status: status || undefined,
202
+ });
203
+
204
+ await deps.sendMessageToActiveTurn(
205
+ `🟢 Presence updated: ${show ?? "available"}${status ? ` (${status})` : ""}`,
206
+ );
207
+ },
208
+ },
209
+ ];
210
+ }
211
+
212
+ function parseArgs(raw: string): Record<string, string> {
213
+ const args: Record<string, string> = {};
214
+ const tokens = raw.match(/(?:--?\w+(?:=\S+|\s+\S+)?|"[^"]*"|'[^']*'|\S+)/g) ?? [];
215
+ let i = 0;
216
+ while (i < tokens.length) {
217
+ const token = tokens[i];
218
+ if (token.startsWith("--")) {
219
+ const eqIdx = token.indexOf("=");
220
+ if (eqIdx >= 0) {
221
+ args[token.slice(2, eqIdx)] = token.slice(eqIdx + 1);
222
+ } else {
223
+ const key = token.slice(2);
224
+ const next = tokens[i + 1];
225
+ if (next && !next.startsWith("-")) {
226
+ args[key] = next.replace(/^["']|["']$/g, "");
227
+ i++;
228
+ } else {
229
+ args[key] = "true";
230
+ }
231
+ }
232
+ } else if (token.startsWith("-") && token.length === 2) {
233
+ const key = token.slice(1);
234
+ const next = tokens[i + 1];
235
+ if (next && !next.startsWith("-")) {
236
+ args[key] = next.replace(/^["']|["']$/g, "");
237
+ i++;
238
+ } else {
239
+ args[key] = "true";
240
+ }
241
+ }
242
+ i++;
243
+ }
244
+ return args;
245
+ }
package/lib/config.ts ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * XMPP bridge config and pairing helpers
3
+ * Zones: xmpp config, pairing, filesystem
4
+ * Owns persisted JID/session pairing state, local config storage, live config controls, and first-user pairing side effects
5
+ */
6
+
7
+ import { existsSync } from "node:fs";
8
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { join, resolve } from "node:path";
11
+
12
+ function getAgentDir(): string {
13
+ return process.env.PI_CODING_AGENT_DIR
14
+ ? resolve(process.env.PI_CODING_AGENT_DIR)
15
+ : join(homedir(), ".pi", "agent");
16
+ }
17
+
18
+ function getConfigPath(): string {
19
+ return join(getAgentDir(), "xmpp.json");
20
+ }
21
+
22
+ export interface XmppTimeConfig {
23
+ injectionMode?: "hidden" | "always" | "interval";
24
+ interval?: number;
25
+ }
26
+
27
+ export interface ResolvedXmppTimeConfig {
28
+ injectionMode: "hidden" | "always" | "interval";
29
+ interval: number;
30
+ timezone: string;
31
+ }
32
+
33
+ export interface XmppConfig {
34
+ jid?: string;
35
+ password?: string;
36
+ service?: string;
37
+ domain?: string;
38
+ resource?: string;
39
+ allowedJid?: string;
40
+ autoReconnect?: boolean;
41
+ autoJoinRooms?: string[];
42
+ inboundHandlers?: XmppInboundHandlerConfig[];
43
+ outboundHandlers?: XmppOutboundHandlerConfig[];
44
+ time?: XmppTimeConfig;
45
+ }
46
+
47
+ export interface XmppInboundHandlerConfig {
48
+ match?: string | string[];
49
+ type?: string | string[];
50
+ template?: string | string[];
51
+ args?: string[];
52
+ defaults?: Record<string, unknown>;
53
+ timeout?: number | string;
54
+ }
55
+
56
+ export interface XmppOutboundHandlerConfig {
57
+ type?: string;
58
+ match?: string | string[];
59
+ output?: string;
60
+ timeout?: number | string;
61
+ }
62
+
63
+ export interface XmppConfigStore {
64
+ get: () => XmppConfig;
65
+ set: (config: XmppConfig) => void;
66
+ update: (mutate: (config: XmppConfig) => void) => void;
67
+ getJid: () => string | undefined;
68
+ hasJid: () => boolean;
69
+ getAllowedJid: () => string | undefined;
70
+ getInboundHandlers: () => XmppInboundHandlerConfig[] | undefined;
71
+ getOutboundHandlers: () => XmppOutboundHandlerConfig[] | undefined;
72
+ setAllowedJid: (jid: string) => void;
73
+ load: () => Promise<void>;
74
+ persist: (config?: XmppConfig) => Promise<void>;
75
+ }
76
+
77
+ export interface XmppConfigStoreOptions {
78
+ initialConfig?: XmppConfig;
79
+ agentDir?: string;
80
+ configPath?: string;
81
+ recordRuntimeEvent?: (
82
+ category: string,
83
+ error: unknown,
84
+ details?: Record<string, unknown>,
85
+ ) => void;
86
+ }
87
+
88
+ export interface XmppInvalidConfigRecovery {
89
+ configPath: string;
90
+ recoveryPath: string;
91
+ error: unknown;
92
+ }
93
+
94
+ function isEmptyXmppConfig(config: XmppConfig): boolean {
95
+ return Object.keys(config).length === 0;
96
+ }
97
+
98
+ function getInvalidXmppConfigRecoveryPath(configPath: string): string {
99
+ return `${configPath}.invalid-${process.pid}-${Date.now()}`;
100
+ }
101
+
102
+ export async function readXmppConfig(
103
+ configPath: string,
104
+ options: {
105
+ onInvalidConfig?: (recovery: XmppInvalidConfigRecovery) => void;
106
+ } = {},
107
+ ): Promise<XmppConfig> {
108
+ if (!existsSync(configPath)) return {};
109
+ const content = await readFile(configPath, "utf8");
110
+ try {
111
+ return JSON.parse(content) as XmppConfig;
112
+ } catch (error) {
113
+ const recoveryPath = getInvalidXmppConfigRecoveryPath(configPath);
114
+ await rename(configPath, recoveryPath);
115
+ options.onInvalidConfig?.({ configPath, recoveryPath, error });
116
+ return {};
117
+ }
118
+ }
119
+
120
+ export async function writeXmppConfig(
121
+ agentDir: string,
122
+ configPath: string,
123
+ config: XmppConfig,
124
+ ): Promise<void> {
125
+ await mkdir(agentDir, { recursive: true });
126
+ const tempConfigPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
127
+ await writeFile(tempConfigPath, JSON.stringify(config, null, "\t") + "\n", {
128
+ encoding: "utf8",
129
+ mode: 0o600,
130
+ });
131
+ await chmod(tempConfigPath, 0o600);
132
+ await rename(tempConfigPath, configPath);
133
+ await chmod(configPath, 0o600);
134
+ }
135
+
136
+ export function createXmppConfigStore(
137
+ options: XmppConfigStoreOptions = {},
138
+ ): XmppConfigStore {
139
+ let config: XmppConfig = options.initialConfig ?? {};
140
+ const agentDir = options.agentDir ?? getAgentDir();
141
+ const configPath = options.configPath ?? getConfigPath();
142
+ return {
143
+ get: () => config,
144
+ set: (nextConfig) => {
145
+ config = nextConfig;
146
+ },
147
+ update: (mutate) => {
148
+ mutate(config);
149
+ },
150
+ getJid: () => config.jid,
151
+ hasJid: () => !!config.jid,
152
+ getAllowedJid: () => config.allowedJid,
153
+ getInboundHandlers: () => config.inboundHandlers,
154
+ getOutboundHandlers: () => config.outboundHandlers,
155
+ setAllowedJid: (jid) => {
156
+ config.allowedJid = jid;
157
+ },
158
+ load: async () => {
159
+ config = await readXmppConfig(configPath, {
160
+ onInvalidConfig: (recovery) => {
161
+ options.recordRuntimeEvent?.("config", recovery.error, {
162
+ phase: "load",
163
+ configPath: recovery.configPath,
164
+ recoveryPath: recovery.recoveryPath,
165
+ });
166
+ },
167
+ });
168
+ },
169
+ persist: async (nextConfig = config) => {
170
+ await writeXmppConfig(agentDir, configPath, nextConfig);
171
+ },
172
+ };
173
+ }
174
+
175
+ function getSystemTimezone(): string {
176
+ try {
177
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
178
+ return tz && tz.length > 0 ? tz : "UTC";
179
+ } catch {
180
+ return "UTC";
181
+ }
182
+ }
183
+
184
+ export function resolveXmppTimeConfig(
185
+ raw: XmppTimeConfig | undefined,
186
+ ): ResolvedXmppTimeConfig {
187
+ const injectionMode: "hidden" | "always" | "interval" =
188
+ raw?.injectionMode === "always" || raw?.injectionMode === "interval"
189
+ ? raw.injectionMode
190
+ : "hidden";
191
+ const interval =
192
+ typeof raw?.interval === "number" && raw.interval > 0
193
+ ? raw.interval
194
+ : 60 * 60 * 1000;
195
+ const timezone = getSystemTimezone();
196
+ return { injectionMode, interval, timezone };
197
+ }
198
+
199
+ export interface XmppAuthorizationState {
200
+ kind: "pair" | "allow" | "deny";
201
+ jid?: string;
202
+ }
203
+
204
+ export function getXmppAuthorizationState(
205
+ fromJid: string,
206
+ allowedJid?: string,
207
+ ): XmppAuthorizationState {
208
+ if (allowedJid === undefined) {
209
+ return { kind: "pair", jid: fromJid };
210
+ }
211
+ if (fromJid === allowedJid) {
212
+ return { kind: "allow" };
213
+ }
214
+ return { kind: "deny" };
215
+ }
216
+
217
+ export function normalizeJid(jid: string): string {
218
+ // Remove resource part for comparison
219
+ const idx = jid.indexOf("/");
220
+ return idx >= 0 ? jid.slice(0, idx) : jid;
221
+ }
package/lib/inbound.ts ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * XMPP inbound handler pipeline
3
+ * Zones: xmpp inbound, message processing, prompt preparation
4
+ * Owns stanza dispatch, handler matching, and prompt injection before enqueueing
5
+ */
6
+
7
+ import type { XmppConfig, XmppInboundHandlerConfig } from "./config.ts";
8
+ import type { XmppMessageRoute } from "./routing.ts";
9
+
10
+ const INBOUND_HANDLER_REGISTRY_KEY = "__piXmppInboundHandlers__";
11
+
12
+ export interface XmppInboundFile {
13
+ path: string;
14
+ fileName?: string;
15
+ mimeType?: string;
16
+ kind?: string;
17
+ }
18
+
19
+ export interface XmppInboundHandlerOutput {
20
+ file: XmppInboundFile;
21
+ output: string;
22
+ handler: XmppInboundHandlerConfig;
23
+ }
24
+
25
+ export interface XmppInboundHandlerProcessResult {
26
+ rawText: string;
27
+ handlerOutputs: string[];
28
+ handled: boolean;
29
+ }
30
+
31
+ export interface XmppInboundProgrammaticHandlerInput {
32
+ body: string;
33
+ from: string;
34
+ fromBare: string;
35
+ type: string;
36
+ isGroup: boolean;
37
+ roomJid?: string;
38
+ senderNick?: string;
39
+ config: XmppConfig;
40
+ }
41
+
42
+ export interface XmppInboundProgrammaticHandlerResult {
43
+ handled: boolean;
44
+ prompt?: string;
45
+ output?: string;
46
+ }
47
+
48
+ export interface XmppInboundProgrammaticHandler {
49
+ (
50
+ input: XmppInboundProgrammaticHandlerInput,
51
+ ): XmppInboundProgrammaticHandlerResult | Promise<XmppInboundProgrammaticHandlerResult>;
52
+ }
53
+
54
+ // Re-export for api surface compatibility
55
+ export type { XmppInboundHandlerConfig };
56
+
57
+ /**
58
+ * Register a programmatic inbound handler.
59
+ * Handlers are called in registration order. The first handler that returns
60
+ * `{ handled: true }` wins and its prompt replaces the default message prompt.
61
+ */
62
+ export function registerXmppInboundHandler(
63
+ handler: XmppInboundProgrammaticHandler,
64
+ ): void {
65
+ const registry = getInboundHandlerRegistry();
66
+ registry.push(handler);
67
+ }
68
+
69
+ function getInboundHandlerRegistry(): XmppInboundProgrammaticHandler[] {
70
+ const globals = globalThis as Record<string, unknown>;
71
+ if (!globals[INBOUND_HANDLER_REGISTRY_KEY]) {
72
+ globals[INBOUND_HANDLER_REGISTRY_KEY] = [];
73
+ }
74
+ return globals[INBOUND_HANDLER_REGISTRY_KEY] as XmppInboundProgrammaticHandler[];
75
+ }
76
+
77
+ export function getXmppInboundHandlers(): XmppInboundProgrammaticHandler[] {
78
+ return getInboundHandlerRegistry();
79
+ }
80
+
81
+ /**
82
+ * Process an incoming message through the inbound handler pipeline.
83
+ * Returns the prompt text and any handler outputs.
84
+ */
85
+ export async function processXmppInbound(
86
+ route: XmppMessageRoute,
87
+ config: XmppConfig,
88
+ ): Promise<XmppInboundHandlerProcessResult> {
89
+ const handlers = getXmppInboundHandlers();
90
+
91
+ for (const handler of handlers) {
92
+ try {
93
+ const result = await handler({
94
+ body: route.body,
95
+ from: route.from,
96
+ fromBare: route.fromBare,
97
+ type: route.type,
98
+ isGroup: route.isGroup,
99
+ roomJid: route.roomJid,
100
+ senderNick: route.senderNick,
101
+ config,
102
+ });
103
+
104
+ if (result.handled) {
105
+ return {
106
+ rawText: result.prompt ?? route.body,
107
+ handlerOutputs: result.output ? [result.output] : [],
108
+ handled: true,
109
+ };
110
+ }
111
+ } catch (error) {
112
+ // Handler error, continue to next
113
+ continue;
114
+ }
115
+ }
116
+
117
+ // Default handling: use the message body as prompt
118
+ return {
119
+ rawText: route.body,
120
+ handlerOutputs: [],
121
+ handled: false,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Check if a message body matches an inbound handler config
127
+ */
128
+ export function matchesInboundHandler(
129
+ body: string,
130
+ handler: XmppInboundHandlerConfig,
131
+ ): boolean {
132
+ if (!handler.match) return true; // Match all if no pattern
133
+ const patterns = Array.isArray(handler.match) ? handler.match : [handler.match];
134
+ return patterns.some((pattern) => {
135
+ if (pattern.startsWith("/") && pattern.endsWith("/")) {
136
+ // Regex pattern
137
+ const regex = new RegExp(pattern.slice(1, -1));
138
+ return regex.test(body);
139
+ }
140
+ // Glob/simple match
141
+ return body.includes(pattern);
142
+ });
143
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * XMPP lifecycle hook registration helpers
3
+ * Zones: pi agent lifecycle, xmpp session
4
+ * Binds prepared XMPP lifecycle runtimes to pi extension lifecycle events
5
+ */
6
+
7
+ import type {
8
+ AgentEndEvent,
9
+ AgentStartEvent,
10
+ BeforeAgentStartEvent,
11
+ ExtensionAPI,
12
+ ExtensionContext,
13
+ SessionBeforeCompactEvent,
14
+ SessionCompactEvent,
15
+ SessionShutdownEvent,
16
+ SessionStartEvent,
17
+ } from "./pi.ts";
18
+
19
+ export interface XmppBeforeAgentStartResult {
20
+ systemPrompt?: string;
21
+ }
22
+
23
+ type XmppBeforeAgentStartReturn =
24
+ | Promise<XmppBeforeAgentStartResult | undefined>
25
+ | XmppBeforeAgentStartResult
26
+ | undefined;
27
+
28
+ export interface XmppLifecycleRegistrationDeps {
29
+ onSessionStart: (
30
+ event: SessionStartEvent,
31
+ ctx: ExtensionContext,
32
+ ) => Promise<void>;
33
+ onSessionShutdown: (
34
+ event: SessionShutdownEvent,
35
+ ctx: ExtensionContext,
36
+ ) => Promise<void>;
37
+ onSessionBeforeCompact?: (
38
+ event: SessionBeforeCompactEvent,
39
+ ctx: ExtensionContext,
40
+ ) => Promise<void> | void;
41
+ onSessionCompact?: (
42
+ event: SessionCompactEvent,
43
+ ctx: ExtensionContext,
44
+ ) => Promise<void> | void;
45
+ onBeforeAgentStart: (
46
+ event: BeforeAgentStartEvent,
47
+ ctx: ExtensionContext,
48
+ ) => XmppBeforeAgentStartReturn;
49
+ onAgentStart: (
50
+ event: AgentStartEvent,
51
+ ctx: ExtensionContext,
52
+ ) => Promise<void>;
53
+ onAgentEnd: (
54
+ event: AgentEndEvent,
55
+ ctx: ExtensionContext,
56
+ ) => Promise<void>;
57
+ }
58
+
59
+ export interface XmppSessionContextStore<TContext> {
60
+ get: () => TContext | undefined;
61
+ set: (ctx: TContext) => void;
62
+ clear: () => void;
63
+ }
64
+
65
+ export function createXmppSessionContextStore<
66
+ TContext,
67
+ >(): XmppSessionContextStore<TContext> {
68
+ let currentContext: TContext | undefined;
69
+ return {
70
+ get: () => currentContext,
71
+ set: (ctx) => {
72
+ currentContext = ctx;
73
+ },
74
+ clear: () => {
75
+ currentContext = undefined;
76
+ },
77
+ };
78
+ }
79
+
80
+ export function registerXmppLifecycleHooks(
81
+ pi: ExtensionAPI,
82
+ deps: XmppLifecycleRegistrationDeps,
83
+ ): void {
84
+ pi.on("session_start", async (event, ctx) => {
85
+ await deps.onSessionStart(event, ctx);
86
+ });
87
+ pi.on("session_shutdown", async (event, ctx) => {
88
+ await deps.onSessionShutdown(event, ctx);
89
+ });
90
+ pi.on("session_before_compact", async (event, ctx) => {
91
+ await deps.onSessionBeforeCompact?.(event, ctx);
92
+ });
93
+ pi.on("session_compact", async (event, ctx) => {
94
+ await deps.onSessionCompact?.(event, ctx);
95
+ });
96
+ pi.on("before_agent_start", async (event, ctx) => {
97
+ return deps.onBeforeAgentStart(event, ctx);
98
+ });
99
+ pi.on("agent_start", async (event, ctx) => {
100
+ await deps.onAgentStart(event, ctx);
101
+ });
102
+ pi.on("agent_end", async (event, ctx) => {
103
+ await deps.onAgentEnd(event, ctx);
104
+ });
105
+ }