@relaymessenger/pi 0.1.0-staging.0 → 0.1.0-staging.1
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/README.md +9 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +103 -56
- package/dist/native.d.ts +7 -0
- package/dist/native.js +44 -0
- package/package.json +36 -7
package/README.md
CHANGED
|
@@ -6,3 +6,12 @@ each inbound Message through Pi RPC mode, and sends one final text reply.
|
|
|
6
6
|
`relay connect pi` configures and runs this path for you. For a direct
|
|
7
7
|
integration, pass `agentToken`, optionally `baseURL` and `piCommand`, then use
|
|
8
8
|
`runPiChannel()`.
|
|
9
|
+
|
|
10
|
+
To load the native Pi extension in an existing Pi installation:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pi install npm:@relaymessenger/pi
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Set `RELAY_AGENT_TOKEN` before using `/relay-connect`. Set
|
|
17
|
+
`RELAY_BASE_URL` when the Agent belongs to a non-default API environment.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export interface PiChannelOptions {
|
|
|
4
4
|
readonly baseURL?: string;
|
|
5
5
|
readonly piCommand?: string;
|
|
6
6
|
readonly piArgs?: readonly string[];
|
|
7
|
-
readonly
|
|
7
|
+
readonly rpcTimeoutMs?: number;
|
|
8
|
+
readonly spawnPi?: (command: string, args: readonly string[], chatId: string) => PiProcess;
|
|
8
9
|
readonly relay?: Relay;
|
|
9
10
|
}
|
|
10
11
|
export interface PiProcess {
|
|
@@ -19,5 +20,6 @@ export declare class PiChannel {
|
|
|
19
20
|
#private;
|
|
20
21
|
constructor(options: PiChannelOptions);
|
|
21
22
|
run(signal?: AbortSignal): Promise<void>;
|
|
23
|
+
stop(): void;
|
|
22
24
|
}
|
|
23
25
|
export declare const runPiChannel: (options: PiChannelOptions, signal?: AbortSignal) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,7 @@ import { createInterface } from "node:readline";
|
|
|
3
3
|
import Relay, {} from "@relaymessenger/sdk";
|
|
4
4
|
class ChildPiProcess {
|
|
5
5
|
#child;
|
|
6
|
-
constructor(command, args) {
|
|
7
|
-
this.#child = spawn(command, [...args], { stdio: ["pipe", "pipe", "pipe"] });
|
|
8
|
-
this.#child.stderr.resume();
|
|
9
|
-
}
|
|
6
|
+
constructor(command, args) { this.#child = spawn(command, [...args], { stdio: ["pipe", "pipe", "pipe"] }); this.#child.stderr.resume(); }
|
|
10
7
|
get stdin() { return this.#child.stdin; }
|
|
11
8
|
get stdout() { return createInterface({ input: this.#child.stdout }); }
|
|
12
9
|
kill = () => { this.#child.kill(); };
|
|
@@ -16,77 +13,127 @@ const textFromEvent = (event) => {
|
|
|
16
13
|
return null;
|
|
17
14
|
return event.data.parts.filter((part) => part.type === "text" || part.type === "link").map((part) => part.value).join("\n").trim() || null;
|
|
18
15
|
};
|
|
16
|
+
class ChatSession {
|
|
17
|
+
process;
|
|
18
|
+
lines;
|
|
19
|
+
settled = false;
|
|
20
|
+
nextId = 0;
|
|
21
|
+
constructor(process) { this.process = process; this.lines = process.stdout[Symbol.asyncIterator](); }
|
|
22
|
+
async read(timeoutMs, signal) {
|
|
23
|
+
if (signal?.aborted)
|
|
24
|
+
throw new Error("Pi RPC request aborted");
|
|
25
|
+
let timer;
|
|
26
|
+
let onAbort;
|
|
27
|
+
try {
|
|
28
|
+
const result = await Promise.race([
|
|
29
|
+
this.lines.next(),
|
|
30
|
+
new Promise((_, reject) => {
|
|
31
|
+
timer = setTimeout(() => reject(new Error("Pi RPC request timed out")), timeoutMs);
|
|
32
|
+
onAbort = () => reject(new Error("Pi RPC request aborted"));
|
|
33
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
if (result.done)
|
|
37
|
+
throw new Error("Pi RPC process exited");
|
|
38
|
+
const record = JSON.parse(result.value);
|
|
39
|
+
if (record.type === "agent_settled")
|
|
40
|
+
this.settled = true;
|
|
41
|
+
return record;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (timer)
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
if (onAbort)
|
|
47
|
+
signal?.removeEventListener("abort", onAbort);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async command(type, data, timeoutMs, signal) {
|
|
51
|
+
const id = String(++this.nextId);
|
|
52
|
+
this.process.stdin.write(`${JSON.stringify({ id, type, ...data })}\n`);
|
|
53
|
+
for (;;) {
|
|
54
|
+
const record = await this.read(timeoutMs, signal);
|
|
55
|
+
if (record.type === "response" && record.id === id) {
|
|
56
|
+
if (record.success === false)
|
|
57
|
+
throw new Error(record.error ?? `Pi RPC ${type} failed`);
|
|
58
|
+
return record;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
stop() { this.process.stdin.end(); this.process.kill(); }
|
|
63
|
+
}
|
|
19
64
|
export class PiChannel {
|
|
20
65
|
#relay;
|
|
21
66
|
#options;
|
|
22
67
|
#spawnPi;
|
|
68
|
+
#sessions = new Map();
|
|
69
|
+
#turns = new Map();
|
|
70
|
+
#seen = new Set();
|
|
71
|
+
#inflight = new Map();
|
|
72
|
+
#abortListener;
|
|
23
73
|
constructor(options) {
|
|
24
74
|
if (!options.agentToken.trim())
|
|
25
75
|
throw new Error("Relay Agent Token is required");
|
|
26
76
|
this.#options = options;
|
|
27
|
-
this.#relay = options.relay ?? new Relay({
|
|
28
|
-
apiKey: options.agentToken,
|
|
29
|
-
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
30
|
-
});
|
|
77
|
+
this.#relay = options.relay ?? new Relay({ apiKey: options.agentToken, ...(options.baseURL ? { baseURL: options.baseURL } : {}) });
|
|
31
78
|
this.#spawnPi = options.spawnPi ?? ((command, args) => new ChildPiProcess(command, args));
|
|
32
79
|
}
|
|
33
80
|
async run(signal) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
onFullSync: async () => { }
|
|
38
|
-
}
|
|
81
|
+
this.#abortListener = () => this.stop();
|
|
82
|
+
signal?.addEventListener("abort", this.#abortListener, { once: true });
|
|
83
|
+
try {
|
|
84
|
+
await this.#relay.websocket.run({ ...(signal ? { signal } : {}), onEvent: async (event) => this.#handle(event, signal), onFullSync: async () => { throw new Error("Pi channel cannot acknowledge FULL sync without a durable Relay inbox"); } });
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.stop();
|
|
88
|
+
if (signal)
|
|
89
|
+
signal.removeEventListener("abort", this.#abortListener);
|
|
90
|
+
}
|
|
39
91
|
}
|
|
40
|
-
|
|
92
|
+
stop() { for (const session of this.#sessions.values())
|
|
93
|
+
session.stop(); this.#sessions.clear(); }
|
|
94
|
+
async #handle(event, signal) {
|
|
95
|
+
if (this.#seen.has(event.event_id))
|
|
96
|
+
return;
|
|
41
97
|
const message = textFromEvent(event);
|
|
42
98
|
if (!message)
|
|
43
99
|
return;
|
|
44
|
-
const
|
|
100
|
+
const data = event.data;
|
|
101
|
+
this.#seen.add(event.event_id);
|
|
102
|
+
const prior = this.#turns.get(data.chat.id) ?? Promise.resolve();
|
|
103
|
+
const turn = prior.then(() => this.#runTurn(event, message, signal));
|
|
104
|
+
this.#turns.set(data.chat.id, turn.catch(() => undefined));
|
|
105
|
+
this.#inflight.set(event.event_id, turn);
|
|
45
106
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (item.done)
|
|
52
|
-
throw new Error("Pi RPC process exited before responding");
|
|
53
|
-
const record = JSON.parse(item.value);
|
|
54
|
-
if (record.type === "response" && record.id === requestId) {
|
|
55
|
-
if (record.success === false)
|
|
56
|
-
throw new Error(record.error ?? `Pi RPC ${command} failed`);
|
|
57
|
-
return record;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
const promptId = String(++id);
|
|
62
|
-
pi.stdin.write(JSON.stringify({ id: promptId, type: "prompt", message }) + "\n");
|
|
63
|
-
await readResponse(promptId, "prompt");
|
|
64
|
-
let settled = false;
|
|
65
|
-
while (!settled) {
|
|
66
|
-
const item = await lines.next();
|
|
67
|
-
if (item.done)
|
|
68
|
-
throw new Error("Pi RPC process exited before settling");
|
|
69
|
-
const record = JSON.parse(item.value);
|
|
70
|
-
settled = record.type === "agent_settled";
|
|
71
|
-
}
|
|
72
|
-
const textId = String(++id);
|
|
73
|
-
pi.stdin.write(JSON.stringify({ id: textId, type: "get_last_assistant_text" }) + "\n");
|
|
74
|
-
const response = await readResponse(textId, "get_last_assistant_text");
|
|
75
|
-
const answer = response.data?.text?.trim();
|
|
76
|
-
if (!answer)
|
|
77
|
-
throw new Error("Pi returned no final text answer");
|
|
78
|
-
const data = event.data;
|
|
79
|
-
await this.#relay.chats.messages.send(data.chat.id, {
|
|
80
|
-
message: {
|
|
81
|
-
parts: [{ type: "text", value: answer }],
|
|
82
|
-
idempotency_key: `pi-${event.event_id}`,
|
|
83
|
-
},
|
|
84
|
-
});
|
|
107
|
+
await turn;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
this.#seen.delete(event.event_id);
|
|
111
|
+
throw error;
|
|
85
112
|
}
|
|
86
113
|
finally {
|
|
87
|
-
|
|
88
|
-
|
|
114
|
+
this.#inflight.delete(event.event_id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async #runTurn(event, message, signal) {
|
|
118
|
+
const data = event.data;
|
|
119
|
+
let session = this.#sessions.get(data.chat.id);
|
|
120
|
+
if (!session) {
|
|
121
|
+
session = new ChatSession(this.#spawnPi(this.#options.piCommand ?? "pi", ["--mode", "rpc", ...(this.#options.piArgs ?? [])], data.chat.id));
|
|
122
|
+
this.#sessions.set(data.chat.id, session);
|
|
123
|
+
}
|
|
124
|
+
const timeout = this.#options.rpcTimeoutMs ?? 60_000;
|
|
125
|
+
await session.command("prompt", { message }, timeout, signal);
|
|
126
|
+
if (!session.settled) {
|
|
127
|
+
while (!session.settled)
|
|
128
|
+
await session.read(timeout, signal);
|
|
89
129
|
}
|
|
130
|
+
const response = await session.command("get_last_assistant_text", {}, timeout, signal);
|
|
131
|
+
const answer = response.data?.text?.trim();
|
|
132
|
+
if (!answer)
|
|
133
|
+
throw new Error("Pi returned no final text answer");
|
|
134
|
+
const chunks = answer.match(/[\s\S]{1,10000}/gu) ?? [];
|
|
135
|
+
for (const [index, chunk] of chunks.entries())
|
|
136
|
+
await this.#relay.chats.messages.send(data.chat.id, { message: { parts: [{ type: "text", value: chunk }], idempotency_key: `pi-${event.event_id}-${index}` } });
|
|
90
137
|
}
|
|
91
138
|
}
|
|
92
139
|
export const runPiChannel = (options, signal) => new PiChannel(options).run(signal);
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Pi-native entry point. It deliberately exposes only lifecycle-safe commands;
|
|
4
|
+
* Relay ingress remains owned by runPiChannel so the CLI and extension share
|
|
5
|
+
* the same authenticated routing implementation.
|
|
6
|
+
*/
|
|
7
|
+
export default function relayPiExtension(pi: ExtensionAPI): void;
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { runPiChannel } from "./index.js";
|
|
2
|
+
let controller;
|
|
3
|
+
let running;
|
|
4
|
+
/**
|
|
5
|
+
* Pi-native entry point. It deliberately exposes only lifecycle-safe commands;
|
|
6
|
+
* Relay ingress remains owned by runPiChannel so the CLI and extension share
|
|
7
|
+
* the same authenticated routing implementation.
|
|
8
|
+
*/
|
|
9
|
+
export default function relayPiExtension(pi) {
|
|
10
|
+
pi.registerCommand("relay-connect", {
|
|
11
|
+
description: "Start the Relay channel using the configured Agent Token",
|
|
12
|
+
handler: async (_args, ctx) => {
|
|
13
|
+
if (running) {
|
|
14
|
+
ctx.ui.notify("Relay Pi channel is already running.", "info");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const agentToken = process.env.RELAY_AGENT_TOKEN?.trim();
|
|
18
|
+
if (!agentToken) {
|
|
19
|
+
ctx.ui.notify("RELAY_AGENT_TOKEN is not configured.", "error");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
controller = new AbortController();
|
|
23
|
+
running = runPiChannel({
|
|
24
|
+
agentToken,
|
|
25
|
+
...(process.env.RELAY_BASE_URL?.trim()
|
|
26
|
+
? { baseURL: process.env.RELAY_BASE_URL.trim() }
|
|
27
|
+
: {}),
|
|
28
|
+
}, controller.signal).catch((error) => {
|
|
29
|
+
ctx.ui.notify(`Relay Pi channel stopped: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
30
|
+
}).finally(() => {
|
|
31
|
+
running = undefined;
|
|
32
|
+
controller = undefined;
|
|
33
|
+
});
|
|
34
|
+
ctx.ui.notify("Relay Pi channel started.", "info");
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
pi.registerCommand("relay-disconnect", {
|
|
38
|
+
description: "Stop the Relay channel started by the Relay CLI",
|
|
39
|
+
handler: async (_args, ctx) => {
|
|
40
|
+
controller?.abort();
|
|
41
|
+
ctx.ui.notify("Relay Pi channel stopping.", "info");
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relaymessenger/pi",
|
|
3
|
-
"version": "0.1.0-staging.
|
|
3
|
+
"version": "0.1.0-staging.1",
|
|
4
4
|
"description": "Relay Messenger channel for Pi over Relay v1 WebSocket delivery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,21 +15,50 @@
|
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"packageManager": "npm@12.0.2",
|
|
18
|
-
"engines": {
|
|
19
|
-
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.22.3"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./dist/native.js"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
20
31
|
"scripts": {
|
|
21
32
|
"build": "tsc -p tsconfig.build.json",
|
|
22
33
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
23
34
|
"test": "vitest run"
|
|
24
35
|
},
|
|
25
|
-
"dependencies": {
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@relaymessenger/sdk": "0.3.3-staging.7"
|
|
38
|
+
},
|
|
26
39
|
"devDependencies": {
|
|
40
|
+
"@mariozechner/pi-coding-agent": "0.73.1",
|
|
27
41
|
"@types/node": "26.0.0",
|
|
28
42
|
"typescript": "7.0.2",
|
|
29
43
|
"vitest": "4.1.10"
|
|
30
44
|
},
|
|
31
|
-
"publishConfig": {
|
|
32
|
-
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/",
|
|
48
|
+
"tag": "staging",
|
|
49
|
+
"provenance": true
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/RelayMessenger/Relay-SDK.git",
|
|
54
|
+
"directory": "packages/pi"
|
|
55
|
+
},
|
|
33
56
|
"homepage": "https://github.com/RelayMessenger/Relay-SDK/tree/main/packages/pi#readme",
|
|
34
|
-
"keywords": [
|
|
57
|
+
"keywords": [
|
|
58
|
+
"relay",
|
|
59
|
+
"pi",
|
|
60
|
+
"pi-package",
|
|
61
|
+
"channel",
|
|
62
|
+
"messaging"
|
|
63
|
+
]
|
|
35
64
|
}
|