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.
- package/AGENTS.md +3 -0
- package/BACKLOG.md +17 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE +9 -0
- package/README.md +124 -0
- package/api/commands.ts +11 -0
- package/api/inbound.ts +14 -0
- package/api/outbound.ts +11 -0
- package/api/status.ts +12 -0
- package/api/updates.ts +41 -0
- package/index.ts +325 -0
- package/lib/commands.ts +245 -0
- package/lib/config.ts +221 -0
- package/lib/inbound.ts +143 -0
- package/lib/lifecycle.ts +105 -0
- package/lib/model.ts +96 -0
- package/lib/outbound.ts +138 -0
- package/lib/pi.ts +165 -0
- package/lib/prompts.ts +95 -0
- package/lib/queue.ts +129 -0
- package/lib/routing.ts +196 -0
- package/lib/runtime.ts +148 -0
- package/lib/status.ts +144 -0
- package/lib/xmpp-api.ts +352 -0
- package/lib/xmpp.d.ts +59 -0
- package/package.json +71 -0
package/AGENTS.md
ADDED
package/BACKLOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Backlog
|
|
2
|
+
|
|
3
|
+
## v0.2.0
|
|
4
|
+
|
|
5
|
+
- [ ] XMPP roster management (fetch and display contacts)
|
|
6
|
+
- [ ] Message delivery receipts (XEP-0184)
|
|
7
|
+
- [ ] Chat state notifications (XEP-0085)
|
|
8
|
+
- [ ] File transfer support (HTTP Upload / XEP-0363)
|
|
9
|
+
- [ ] End-to-end encryption (OMEMO / XEP-0384)
|
|
10
|
+
- [ ] Formatted messages (XEP-0071 XHTML-IM)
|
|
11
|
+
|
|
12
|
+
## v0.3.0
|
|
13
|
+
|
|
14
|
+
- [ ] Multi-instance bus (like pi-telegram's threaded mode)
|
|
15
|
+
- [ ] Voice message support
|
|
16
|
+
- [ ] Message correction (XEP-0308)
|
|
17
|
+
- [ ] Last Message Correction (XEP-0308)
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
- Initial release
|
|
6
|
+
- XMPP connection management (connect/disconnect/auto-reconnect)
|
|
7
|
+
- Direct message send/receive
|
|
8
|
+
- MUC/groupchat support (join/leave/participate)
|
|
9
|
+
- Presence management
|
|
10
|
+
- Authorization and auto-pairing
|
|
11
|
+
- Slash commands (/xmpp-connect, /xmpp-disconnect, /xmpp-status, /xmpp-join, /xmpp-leave, /xmpp-set-presence)
|
|
12
|
+
- Programmatic inbound/outbound handler API
|
|
13
|
+
- Companion extension API (status providers, commands, update handlers)
|
|
14
|
+
- Configuration via ~/.pi/agent/xmpp.json
|
|
15
|
+
- Auto-join rooms on connect
|
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stan Kondrat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# pi-xmpp
|
|
2
|
+
|
|
3
|
+
XMPP runtime adapter for [Pi](https://github.com/earendil-works/pi).
|
|
4
|
+
|
|
5
|
+
Connect your Pi agent to Jabber/XMPP servers for instant messaging with AI assistance.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **XMPP Connection Management** — Connect, disconnect, and auto-reconnect to any XMPP server
|
|
10
|
+
- **Direct Messaging** — Receive and respond to one-on-one chat messages
|
|
11
|
+
- **MUC/Groupchat Support** — Join, leave, and participate in multi-user chat rooms
|
|
12
|
+
- **Presence Management** — Set your availability and status message
|
|
13
|
+
- **Authorization & Pairing** — Auto-pair on first message, restrict to specific JIDs
|
|
14
|
+
- **Companion Extension API** — Register inbound/outbound handlers, status providers, and slash commands
|
|
15
|
+
- **Auto-join Rooms** — Automatically join configured chat rooms on connect
|
|
16
|
+
- **Slash Commands** — Control the bridge via `/xmpp-connect`, `/xmpp-status`, `/xmpp-join`, and more
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install pi-xmpp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
1. **Configure your credentials** in `~/.pi/agent/xmpp.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"jid": "your-username@your-server.org",
|
|
31
|
+
"password": "your-password",
|
|
32
|
+
"service": "xmpp://your-server.org"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. **Start Pi** and the bridge auto-connects.
|
|
37
|
+
|
|
38
|
+
3. **Or connect manually** from within Pi:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
/xmpp-connect --jid user@domain.tld --password secret --service xmpp://server.tld
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Slash Commands
|
|
45
|
+
|
|
46
|
+
| Command | Description |
|
|
47
|
+
|---------|-------------|
|
|
48
|
+
| `/xmpp-connect` | Connect to XMPP server (--jid, --password, --service) |
|
|
49
|
+
| `/xmpp-disconnect` | Disconnect from XMPP server |
|
|
50
|
+
| `/xmpp-status` | Show connection status and diagnostics |
|
|
51
|
+
| `/xmpp-join` | Join a MUC room (--room, --nick) |
|
|
52
|
+
| `/xmpp-leave` | Leave a MUC room (--room) |
|
|
53
|
+
| `/xmpp-set-presence` | Set presence (--show, --status) |
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
Configuration is stored in `~/.pi/agent/xmpp.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"jid": "user@domain.tld",
|
|
62
|
+
"password": "secret",
|
|
63
|
+
"service": "xmpp://server.tld",
|
|
64
|
+
"domain": "server.tld",
|
|
65
|
+
"resource": "pi-bridge",
|
|
66
|
+
"allowedJid": "trusted@domain.tld",
|
|
67
|
+
"autoReconnect": true,
|
|
68
|
+
"autoJoinRooms": ["room@conference.tld"]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Extension API
|
|
73
|
+
|
|
74
|
+
### Inbound Handlers
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { registerXmppInboundHandler } from "pi-xmpp/inbound";
|
|
78
|
+
|
|
79
|
+
registerXmppInboundHandler(async (input) => {
|
|
80
|
+
if (input.body.startsWith("!ping")) {
|
|
81
|
+
return { handled: true, prompt: "User requested ping" };
|
|
82
|
+
}
|
|
83
|
+
return { handled: false };
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Outbound Handlers
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { registerXmppOutboundHandler } from "pi-xmpp/outbound";
|
|
91
|
+
|
|
92
|
+
registerXmppOutboundHandler(async (input) => {
|
|
93
|
+
return { handled: false };
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Status Providers
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { registerXmppStatusLineProvider } from "pi-xmpp/status";
|
|
101
|
+
|
|
102
|
+
registerXmppStatusLineProvider((ctx) => ({
|
|
103
|
+
label: "My Custom Status",
|
|
104
|
+
value: "ok",
|
|
105
|
+
}));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Architecture
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
|
112
|
+
│ XMPP Server│◄───►│ xmpp.js │◄───►│ pi-xmpp │
|
|
113
|
+
│ │ │ Client │ │ Extension │
|
|
114
|
+
└─────────────┘ └──────────────┘ └──────┬──────┘
|
|
115
|
+
│
|
|
116
|
+
┌──────▼──────┐
|
|
117
|
+
│ Pi Agent │
|
|
118
|
+
│ Runtime │
|
|
119
|
+
└─────────────┘
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
package/api/commands.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public XMPP commands API
|
|
3
|
+
* Zones: package boundary, companion extension interop
|
|
4
|
+
* Exposes the stable XMPP slash-command registration surface
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
registerXmppCommand,
|
|
9
|
+
type XmppExtensionCommandContext,
|
|
10
|
+
type XmppExtensionCommandRegistration,
|
|
11
|
+
} from "../lib/commands.ts";
|
package/api/inbound.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public XMPP inbound API
|
|
3
|
+
* Zones: package boundary, companion extension interop
|
|
4
|
+
* Exposes the stable programmatic inbound handler surface
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
registerXmppInboundHandler,
|
|
9
|
+
type XmppInboundHandlerFile,
|
|
10
|
+
type XmppInboundHandlerOutput,
|
|
11
|
+
type XmppInboundProgrammaticHandler,
|
|
12
|
+
type XmppInboundProgrammaticHandlerInput,
|
|
13
|
+
type XmppInboundProgrammaticHandlerResult,
|
|
14
|
+
} from "../lib/inbound.ts";
|
package/api/outbound.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public XMPP outbound API
|
|
3
|
+
* Zones: package boundary, companion extension interop
|
|
4
|
+
* Exposes stable outbound handler and diagnostics surfaces
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
recordXmppRuntimeEvent,
|
|
9
|
+
registerXmppOutboundHandler,
|
|
10
|
+
type XmppOutboundProgrammaticHandler,
|
|
11
|
+
} from "../lib/outbound.ts";
|
package/api/status.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public XMPP status API
|
|
3
|
+
* Zones: package boundary, companion extension interop
|
|
4
|
+
* Exposes compact status-line registration for companion extensions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
registerXmppStatusLineProvider,
|
|
9
|
+
type XmppStatusLineProvider,
|
|
10
|
+
type XmppStatusLineProviderContext,
|
|
11
|
+
type XmppStatusLineProviderResult,
|
|
12
|
+
} from "../lib/status.ts";
|
package/api/updates.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public XMPP updates API
|
|
3
|
+
* Zones: package boundary, companion extension interop
|
|
4
|
+
* Exposes the stable stanza handler surface
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { XmppStanza } from "../lib/xmpp-api.ts";
|
|
8
|
+
|
|
9
|
+
export type XmppUpdateHandlerVerdict =
|
|
10
|
+
| { handled: true }
|
|
11
|
+
| { handled: false; reason?: string };
|
|
12
|
+
|
|
13
|
+
export interface XmppUpdateHandler {
|
|
14
|
+
(stanza: XmppStanza): XmppUpdateHandlerVerdict | Promise<XmppUpdateHandlerVerdict>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const UPDATE_HANDLER_REGISTRY_KEY = "__piXmppUpdateHandlers__";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register a raw stanza update handler.
|
|
21
|
+
* Handlers receive every incoming stanza and can mark it as handled
|
|
22
|
+
* to prevent further processing by the default message pipeline.
|
|
23
|
+
*/
|
|
24
|
+
export function registerXmppUpdateHandler(
|
|
25
|
+
handler: XmppUpdateHandler,
|
|
26
|
+
): void {
|
|
27
|
+
const registry = getUpdateHandlerRegistry();
|
|
28
|
+
registry.push(handler);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getUpdateHandlerRegistry(): XmppUpdateHandler[] {
|
|
32
|
+
const globals = globalThis as Record<string, unknown>;
|
|
33
|
+
if (!globals[UPDATE_HANDLER_REGISTRY_KEY]) {
|
|
34
|
+
globals[UPDATE_HANDLER_REGISTRY_KEY] = [];
|
|
35
|
+
}
|
|
36
|
+
return globals[UPDATE_HANDLER_REGISTRY_KEY] as XmppUpdateHandler[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getXmppUpdateHandlers(): XmppUpdateHandler[] {
|
|
40
|
+
return getUpdateHandlerRegistry();
|
|
41
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMPP bridge extension entrypoint and orchestration layer
|
|
3
|
+
* Zones: xmpp, pi agent, orchestration
|
|
4
|
+
* Keeps the runtime wiring in one place while delegating reusable domain logic to /lib modules
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Pi from "./lib/pi.ts";
|
|
8
|
+
import * as Commands from "./lib/commands.ts";
|
|
9
|
+
import * as Config from "./lib/config.ts";
|
|
10
|
+
import * as Inbound from "./lib/inbound.ts";
|
|
11
|
+
import * as Lifecycle from "./lib/lifecycle.ts";
|
|
12
|
+
import * as Model from "./lib/model.ts";
|
|
13
|
+
import * as Outbound from "./lib/outbound.ts";
|
|
14
|
+
import * as Prompts from "./lib/prompts.ts";
|
|
15
|
+
import * as Queue from "./lib/queue.ts";
|
|
16
|
+
import * as Routing from "./lib/routing.ts";
|
|
17
|
+
import * as Runtime from "./lib/runtime.ts";
|
|
18
|
+
import * as Status from "./lib/status.ts";
|
|
19
|
+
import * as XmppApi from "./lib/xmpp-api.ts";
|
|
20
|
+
import { Type } from "@sinclair/typebox";
|
|
21
|
+
import * as Updates from "./api/updates.ts";
|
|
22
|
+
|
|
23
|
+
// --- Extension Runtime ---
|
|
24
|
+
|
|
25
|
+
export default function (pi: Pi.ExtensionAPI) {
|
|
26
|
+
const piRuntime = Pi.createExtensionApiRuntimePorts(pi);
|
|
27
|
+
const { sendUserMessage } = piRuntime;
|
|
28
|
+
|
|
29
|
+
// --- XMPP Client ---
|
|
30
|
+
const xmppClient = XmppApi.createXmppClient();
|
|
31
|
+
|
|
32
|
+
// --- Instance identity ---
|
|
33
|
+
const xmppInstanceId = `${process.pid}:${Date.now()}`;
|
|
34
|
+
|
|
35
|
+
// --- Runtime ---
|
|
36
|
+
const bridgeRuntime = Runtime.createXmppBridgeRuntime();
|
|
37
|
+
const { abort, lifecycle, queue } = bridgeRuntime;
|
|
38
|
+
|
|
39
|
+
// --- Config ---
|
|
40
|
+
const runtimeEvents = Status.createXmppRuntimeEventRecorder();
|
|
41
|
+
const configStore = Config.createXmppConfigStore({ recordRuntimeEvent: runtimeEvents.record });
|
|
42
|
+
Outbound.bindXmppRuntimeEventRecorder(runtimeEvents.record);
|
|
43
|
+
|
|
44
|
+
const recordRuntimeEvent = function (
|
|
45
|
+
category: string,
|
|
46
|
+
error: unknown,
|
|
47
|
+
details?: Record<string, unknown>,
|
|
48
|
+
) {
|
|
49
|
+
runtimeEvents.record(category, error ?? undefined, details);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// --- Context store ---
|
|
53
|
+
const sessionContextStore = Lifecycle.createXmppSessionContextStore<Pi.ExtensionContext>();
|
|
54
|
+
|
|
55
|
+
// --- Active turn store ---
|
|
56
|
+
const activeTurnRuntime = Queue.createXmppActiveTurnStore();
|
|
57
|
+
|
|
58
|
+
// --- Queue store ---
|
|
59
|
+
const xmppQueueStore = Queue.createXmppQueueStore<Pi.ExtensionContext>();
|
|
60
|
+
|
|
61
|
+
// --- Status helpers ---
|
|
62
|
+
const getAllowedJid = () => configStore.getAllowedJid();
|
|
63
|
+
const getConnectionStatus = () => xmppClient.status;
|
|
64
|
+
const getActiveTurnFrom = () => activeTurnRuntime.get()?.fromBare;
|
|
65
|
+
|
|
66
|
+
const sendMessageToActiveTurn = async function (body: string): Promise<void> {
|
|
67
|
+
sendUserMessage(body);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// --- Incoming stanza handler ---
|
|
71
|
+
const handleIncomingStanza = async function (stanza: XmppApi.XmppStanza): Promise<void> {
|
|
72
|
+
// Run through update handlers first
|
|
73
|
+
const updateHandlers = Updates.getXmppUpdateHandlers();
|
|
74
|
+
for (const handler of updateHandlers) {
|
|
75
|
+
try {
|
|
76
|
+
const verdict = await handler(stanza);
|
|
77
|
+
if (verdict.handled) return;
|
|
78
|
+
} catch {
|
|
79
|
+
// Continue to next handler
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Route the stanza
|
|
84
|
+
const route = Routing.routeStanza(stanza);
|
|
85
|
+
if (!route) return;
|
|
86
|
+
|
|
87
|
+
if (route.kind === "message") {
|
|
88
|
+
await handleIncomingMessage(route);
|
|
89
|
+
} else if (route.kind === "presence") {
|
|
90
|
+
await handleIncomingPresence(route);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// --- Incoming message handler ---
|
|
95
|
+
const handleIncomingMessage = async function (route: Routing.XmppMessageRoute): Promise<void> {
|
|
96
|
+
// Skip error messages
|
|
97
|
+
if (route.type === "error") return;
|
|
98
|
+
|
|
99
|
+
// Check authorization
|
|
100
|
+
const auth = Config.getXmppAuthorizationState(
|
|
101
|
+
route.fromBare,
|
|
102
|
+
configStore.getAllowedJid(),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (auth.kind === "deny") return;
|
|
106
|
+
|
|
107
|
+
// Auto-pair if needed
|
|
108
|
+
if (auth.kind === "pair") {
|
|
109
|
+
configStore.setAllowedJid(route.fromBare);
|
|
110
|
+
await configStore.persist();
|
|
111
|
+
recordRuntimeEvent("pair", null, { jid: route.fromBare });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Process through inbound handler pipeline
|
|
115
|
+
const config = configStore.get();
|
|
116
|
+
const inboundResult = await Inbound.processXmppInbound(route, config);
|
|
117
|
+
|
|
118
|
+
// Build turn context
|
|
119
|
+
const turn: Queue.XmppTurnContext = {
|
|
120
|
+
from: route.from,
|
|
121
|
+
fromBare: route.fromBare,
|
|
122
|
+
body: inboundResult.rawText,
|
|
123
|
+
type: route.type,
|
|
124
|
+
thread: route.thread,
|
|
125
|
+
subject: route.subject,
|
|
126
|
+
isGroup: route.isGroup,
|
|
127
|
+
roomJid: route.roomJid,
|
|
128
|
+
senderNick: route.senderNick,
|
|
129
|
+
timestamp: Date.now(),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Build prompt
|
|
133
|
+
const prompt = Queue.buildXmppTurnPrompt(turn, {
|
|
134
|
+
includeThread: true,
|
|
135
|
+
extraContext: inboundResult.handlerOutputs.length > 0
|
|
136
|
+
? `handler outputs: ${inboundResult.handlerOutputs.join("; ")}`
|
|
137
|
+
: undefined,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Enqueue the message
|
|
141
|
+
const ctx = sessionContextStore.get();
|
|
142
|
+
if (ctx) {
|
|
143
|
+
const item: Queue.XmppQueueItem<Pi.ExtensionContext> = {
|
|
144
|
+
id: `xmpp-${queue.allocateItemOrder()}-${Date.now()}`,
|
|
145
|
+
order: queue.allocateItemOrder(),
|
|
146
|
+
turn,
|
|
147
|
+
prompt,
|
|
148
|
+
ctx,
|
|
149
|
+
status: "queued",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
xmppQueueStore.enqueue(item);
|
|
153
|
+
dispatchNextQueuedTurn();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// --- Incoming presence handler ---
|
|
158
|
+
const handleIncomingPresence = async function (
|
|
159
|
+
route: Routing.XmppPresenceRoute,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
recordRuntimeEvent("presence", null, {
|
|
162
|
+
from: route.from,
|
|
163
|
+
type: route.type,
|
|
164
|
+
show: route.show,
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// --- Dispatch next queued turn ---
|
|
169
|
+
const dispatchNextQueuedTurn = function (): void {
|
|
170
|
+
if (bridgeRuntime.state.xmppTurnDispatchPending) return;
|
|
171
|
+
if (bridgeRuntime.state.compactionInProgress) return;
|
|
172
|
+
if (activeTurnRuntime.has()) return;
|
|
173
|
+
|
|
174
|
+
const next = xmppQueueStore.dequeue();
|
|
175
|
+
if (!next) return;
|
|
176
|
+
|
|
177
|
+
bridgeRuntime.state.xmppTurnDispatchPending = true;
|
|
178
|
+
activeTurnRuntime.set(next.turn);
|
|
179
|
+
|
|
180
|
+
const ctx = next.ctx;
|
|
181
|
+
|
|
182
|
+
// Set abort handler
|
|
183
|
+
const abortController = new AbortController();
|
|
184
|
+
abort.setHandler(() => {
|
|
185
|
+
abortController.abort();
|
|
186
|
+
activeTurnRuntime.clear();
|
|
187
|
+
bridgeRuntime.state.xmppTurnDispatchPending = false;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Send the prompt to Pi
|
|
191
|
+
// Note: activeTurnRuntime is cleared in onAgentEnd after the agent finishes processing
|
|
192
|
+
try {
|
|
193
|
+
sendUserMessage(next.prompt);
|
|
194
|
+
} catch (error: unknown) {
|
|
195
|
+
recordRuntimeEvent("dispatch", error);
|
|
196
|
+
activeTurnRuntime.clear();
|
|
197
|
+
bridgeRuntime.state.xmppTurnDispatchPending = false;
|
|
198
|
+
abort.clearHandler();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// --- Register slash commands ---
|
|
203
|
+
const allCommands = Commands.createXmppSlashCommands({
|
|
204
|
+
client: xmppClient,
|
|
205
|
+
configStore,
|
|
206
|
+
runtime: bridgeRuntime,
|
|
207
|
+
getConnectionStatus,
|
|
208
|
+
sendMessageToActiveTurn,
|
|
209
|
+
updateStatus: () => {
|
|
210
|
+
// Status is pushed declaratively via getConnectionStatus/getAllowedJid;
|
|
211
|
+
// individual commands that change state can call this to trigger a status refresh.
|
|
212
|
+
},
|
|
213
|
+
getAllowedJid,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
for (const cmd of allCommands) {
|
|
217
|
+
pi.registerCommand(cmd.name, {
|
|
218
|
+
description: cmd.description,
|
|
219
|
+
handler: async (_args: string, _ctx: Pi.ExtensionCommandContext) => {
|
|
220
|
+
await cmd.handler({ args: [], rawArgs: _args });
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Register the xmpp_send tool ---
|
|
226
|
+
pi.registerTool({
|
|
227
|
+
name: "xmpp_send",
|
|
228
|
+
label: "Send XMPP Message",
|
|
229
|
+
description:
|
|
230
|
+
"Send a message via XMPP. Provide `to` (JID), `body` (message text), and optionally `type` (chat or groupchat).",
|
|
231
|
+
parameters: Type.Object({
|
|
232
|
+
to: Type.String({ description: "Recipient JID" }),
|
|
233
|
+
body: Type.String({ description: "Message body" }),
|
|
234
|
+
type: Type.Optional(Type.String({ description: "Message type (default: chat)" })),
|
|
235
|
+
subject: Type.Optional(Type.String({ description: "Optional message subject" })),
|
|
236
|
+
}),
|
|
237
|
+
execute: async (_toolCallId: string, params: { to: string; body: string; type?: string; subject?: string }) => {
|
|
238
|
+
try {
|
|
239
|
+
await Outbound.sendXmppMessage(xmppClient, params.to, params.body, {
|
|
240
|
+
type: params.type,
|
|
241
|
+
subject: params.subject,
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text" as const, text: `Message sent to ${params.to}` }],
|
|
245
|
+
details: {},
|
|
246
|
+
};
|
|
247
|
+
} catch (error: unknown) {
|
|
248
|
+
return {
|
|
249
|
+
content: [
|
|
250
|
+
{
|
|
251
|
+
type: "text" as const,
|
|
252
|
+
text: `Failed to send message: ${error instanceof Error ? error.message : String(error)}`,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
isError: true,
|
|
256
|
+
details: {},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// --- Register the xmpp_help tool ---
|
|
263
|
+
Prompts.registerXmppHelpTool(pi);
|
|
264
|
+
|
|
265
|
+
// --- Lifecycle hooks ---
|
|
266
|
+
const beforeAgentStartHook = Prompts.createXmppBeforeAgentStartHook();
|
|
267
|
+
|
|
268
|
+
Lifecycle.registerXmppLifecycleHooks(pi, {
|
|
269
|
+
onSessionStart: async (_event, ctx) => {
|
|
270
|
+
sessionContextStore.set(ctx);
|
|
271
|
+
|
|
272
|
+
// Try to auto-connect if credentials are stored
|
|
273
|
+
const config = configStore.get();
|
|
274
|
+
if (config.jid && config.password && xmppClient.status === "offline") {
|
|
275
|
+
try {
|
|
276
|
+
await xmppClient.connect(config);
|
|
277
|
+
|
|
278
|
+
// Auto-join configured rooms
|
|
279
|
+
if (config.autoJoinRooms?.length) {
|
|
280
|
+
const nick = config.jid.split("@")[0];
|
|
281
|
+
for (const room of config.autoJoinRooms) {
|
|
282
|
+
xmppClient.joinRoom(room, nick);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
recordRuntimeEvent("auto-connect", error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
onSessionShutdown: async () => {
|
|
292
|
+
sessionContextStore.clear();
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
onBeforeAgentStart: async (event) => {
|
|
296
|
+
return beforeAgentStartHook(event);
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
onAgentStart: async () => {
|
|
300
|
+
// Agent started processing
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
onAgentEnd: async () => {
|
|
304
|
+
// Agent finished processing; clear active turn and dispatch next queued
|
|
305
|
+
activeTurnRuntime.clear();
|
|
306
|
+
bridgeRuntime.state.xmppTurnDispatchPending = false;
|
|
307
|
+
abort.clearHandler();
|
|
308
|
+
dispatchNextQueuedTurn();
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// --- Stanza listener ---
|
|
313
|
+
xmppClient.onStanza(handleIncomingStanza);
|
|
314
|
+
|
|
315
|
+
// --- Record extension start ---
|
|
316
|
+
recordRuntimeEvent("extension-start", null, {
|
|
317
|
+
instanceId: xmppInstanceId,
|
|
318
|
+
pid: process.pid,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// --- Handle uncaught XMPP errors ---
|
|
322
|
+
xmppClient.onError((error) => {
|
|
323
|
+
recordRuntimeEvent("xmpp-error", error);
|
|
324
|
+
});
|
|
325
|
+
}
|