@relaymessenger/pi 0.1.4-staging.2 → 0.1.4-staging.20
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 +20 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +49 -6
- package/dist/native.d.ts +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -15,3 +15,23 @@ pi install npm:@relaymessenger/pi
|
|
|
15
15
|
|
|
16
16
|
Set `RELAY_AGENT_TOKEN` before using `/relay-connect`. Set
|
|
17
17
|
`RELAY_BASE_URL` when the Agent belongs to a non-default API environment.
|
|
18
|
+
|
|
19
|
+
## Selection
|
|
20
|
+
|
|
21
|
+
End the final Pi answer with a `selection` JSON fence after the question.
|
|
22
|
+
The RPC prompt preserves ordered rich parts, `selected_values`, and `reply_to`
|
|
23
|
+
as data. FULL sync still fails closed rather than discarding skipped context.
|
|
24
|
+
|
|
25
|
+
New human reply text is literal `• ` + each selected source label joined with
|
|
26
|
+
`\n`, followed by `selection_response` metadata in source-option order. Dispatch
|
|
27
|
+
with `selected_values` and the explicit source target, never label parsing.
|
|
28
|
+
Exact legacy comma-joined text remains a server compatibility input. The person
|
|
29
|
+
checks any number of options and submits them once; checking sends nothing, and
|
|
30
|
+
a person answers a given selection once. iOS may draw a checkmark in place of
|
|
31
|
+
each bullet and repeat the prompt's title, as presentation only.
|
|
32
|
+
|
|
33
|
+
## Invoice
|
|
34
|
+
|
|
35
|
+
A verified agent ends the final Pi answer with an `invoice` JSON fence holding
|
|
36
|
+
a Stripe checkout link; the words go first and the invoice follows as its own
|
|
37
|
+
Message.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Relay from "@relaymessenger/sdk";
|
|
1
|
+
import Relay, { type MessagePart } from "@relaymessenger/sdk";
|
|
2
2
|
export interface PiChannelOptions {
|
|
3
3
|
readonly agentToken: string;
|
|
4
4
|
readonly baseURL?: string;
|
|
@@ -16,6 +16,22 @@ export interface PiProcess {
|
|
|
16
16
|
readonly stdout: AsyncIterable<string>;
|
|
17
17
|
readonly kill: () => void;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The prompt pi is given for one message: the words, then how to answer.
|
|
21
|
+
* This process sends pi's final text for it, and the same buttons and link
|
|
22
|
+
* rules every other runtime carries.
|
|
23
|
+
*/
|
|
24
|
+
export declare const piPrompt: (message: string) => string;
|
|
25
|
+
/**
|
|
26
|
+
* The messages an answer becomes: each link written alone on a line as its
|
|
27
|
+
* own message, text in chunks the API takes, and the buttons its fenced block
|
|
28
|
+
* asked for under the last words. A block pi wrote that cannot be read stays
|
|
29
|
+
* in the words, so nothing the person was told is lost.
|
|
30
|
+
*/
|
|
31
|
+
export declare const answerMessages: (answer: string) => {
|
|
32
|
+
parts: MessagePart[];
|
|
33
|
+
error?: string;
|
|
34
|
+
}[];
|
|
19
35
|
export declare class PiChannel {
|
|
20
36
|
#private;
|
|
21
37
|
constructor(options: PiChannelOptions);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
import Relay, {} from "@relaymessenger/sdk";
|
|
3
|
+
import Relay, { BUTTONS_BLOCK_INSTRUCTION, BUTTONS_GUIDANCE, INVOICE_BLOCK_INSTRUCTION, INVOICE_GUIDANCE, selectionReply, selectionReplyContext, SELECTION_GUIDANCE, SELECTION_BLOCK_INSTRUCTION, LINK_LINE_INSTRUCTION, answerMessages as splitAnswer, } from "@relaymessenger/sdk";
|
|
4
4
|
class ChildPiProcess {
|
|
5
5
|
#child;
|
|
6
6
|
constructor(command, args) { this.#child = spawn(command, [...args], { stdio: ["pipe", "pipe", "pipe"] }); this.#child.stderr.resume(); }
|
|
@@ -11,7 +11,44 @@ class ChildPiProcess {
|
|
|
11
11
|
const textFromEvent = (event) => {
|
|
12
12
|
if (event.event_type !== "message.received" || event.data.direction !== "inbound")
|
|
13
13
|
return null;
|
|
14
|
-
|
|
14
|
+
const text = event.data.parts
|
|
15
|
+
.flatMap((part) => part.type === "text" || part.type === "link" ? [part.value] : [])
|
|
16
|
+
.join("\n").trim();
|
|
17
|
+
const context = selectionReplyContext(selectionReply(event.data.parts, event.data.reply_to), {
|
|
18
|
+
parts: event.data.parts, ...(event.data.reply_to ? { reply_to: event.data.reply_to } : {}),
|
|
19
|
+
});
|
|
20
|
+
if (!text && !context)
|
|
21
|
+
return null;
|
|
22
|
+
return [text, context].filter(Boolean).join("\n\n");
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The prompt pi is given for one message: the words, then how to answer.
|
|
26
|
+
* This process sends pi's final text for it, and the same buttons and link
|
|
27
|
+
* rules every other runtime carries.
|
|
28
|
+
*/
|
|
29
|
+
export const piPrompt = (message) => `${message}\n\nWrite your answer as your final message. Relay sends that answer to the chat for you, so do not send it yourself. Write chat text. Inline Markdown draws: bold, italic, strikethrough, code, links. Headings, lists and code fences show as written.\n\n${BUTTONS_BLOCK_INSTRUCTION} ${LINK_LINE_INSTRUCTION} ${BUTTONS_GUIDANCE} ${SELECTION_BLOCK_INSTRUCTION} ${SELECTION_GUIDANCE} ${INVOICE_BLOCK_INSTRUCTION} ${INVOICE_GUIDANCE}`;
|
|
30
|
+
/**
|
|
31
|
+
* The messages an answer becomes: each link written alone on a line as its
|
|
32
|
+
* own message, text in chunks the API takes, and the buttons its fenced block
|
|
33
|
+
* asked for under the last words. A block pi wrote that cannot be read stays
|
|
34
|
+
* in the words, so nothing the person was told is lost.
|
|
35
|
+
*/
|
|
36
|
+
export const answerMessages = (answer) => {
|
|
37
|
+
const split = splitAnswer(answer);
|
|
38
|
+
const messages = [];
|
|
39
|
+
for (const [first, ...rest] of split.messages) {
|
|
40
|
+
if (first?.type !== "text" || first.value.length <= 10_000) {
|
|
41
|
+
messages.push({ parts: first ? [first, ...rest] : rest });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const chunks = first.value.match(/[\s\S]{1,10000}/gu) ?? [];
|
|
45
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
46
|
+
messages.push({ parts: [{ type: "text", value: chunk }, ...(index === chunks.length - 1 ? rest : [])] });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (split.error && messages[0])
|
|
50
|
+
messages[0].error = split.error;
|
|
51
|
+
return messages;
|
|
15
52
|
};
|
|
16
53
|
class ChatSession {
|
|
17
54
|
process;
|
|
@@ -92,6 +129,10 @@ export class PiChannel {
|
|
|
92
129
|
stop() { for (const session of this.#sessions.values())
|
|
93
130
|
session.stop(); this.#sessions.clear(); }
|
|
94
131
|
async #handle(event, signal) {
|
|
132
|
+
// Concurrent redelivery must await the original handoff, not ACK early.
|
|
133
|
+
const inflight = this.#inflight.get(event.event_id);
|
|
134
|
+
if (inflight)
|
|
135
|
+
return inflight;
|
|
95
136
|
if (this.#seen.has(event.event_id))
|
|
96
137
|
return;
|
|
97
138
|
const message = textFromEvent(event);
|
|
@@ -122,7 +163,7 @@ export class PiChannel {
|
|
|
122
163
|
this.#sessions.set(data.chat.id, session);
|
|
123
164
|
}
|
|
124
165
|
const timeout = this.#options.rpcTimeoutMs ?? 60_000;
|
|
125
|
-
await session.command("prompt", { message }, timeout, signal);
|
|
166
|
+
await session.command("prompt", { message: piPrompt(message) }, timeout, signal);
|
|
126
167
|
if (!session.settled) {
|
|
127
168
|
while (!session.settled)
|
|
128
169
|
await session.read(timeout, signal);
|
|
@@ -131,9 +172,11 @@ export class PiChannel {
|
|
|
131
172
|
const answer = response.data?.text?.trim();
|
|
132
173
|
if (!answer)
|
|
133
174
|
throw new Error("Pi returned no final text answer");
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
175
|
+
const messages = answerMessages(answer);
|
|
176
|
+
if (messages[0]?.error)
|
|
177
|
+
console.error(`Relay: the component block in pi's answer was left as text: ${messages[0].error}.`);
|
|
178
|
+
for (const [index, message] of messages.entries())
|
|
179
|
+
await this.#relay.chats.messages.send(data.chat.id, { message: { parts: message.parts, idempotency_key: `pi-${event.event_id}-${index}` } });
|
|
137
180
|
}
|
|
138
181
|
}
|
|
139
182
|
export const runPiChannel = (options, signal) => new PiChannel(options).run(signal);
|
package/dist/native.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
/**
|
|
3
3
|
* Pi-native entry point. It deliberately exposes only lifecycle-safe commands;
|
|
4
4
|
* Relay ingress remains owned by runPiChannel so the CLI and extension share
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relaymessenger/pi",
|
|
3
|
-
"version": "0.1.4-staging.
|
|
3
|
+
"version": "0.1.4-staging.20",
|
|
4
4
|
"description": "Relay Messenger channel for Pi over Relay v1 WebSocket delivery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"test": "vitest run"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@relaymessenger/sdk": "0.3.6-staging.
|
|
37
|
+
"@relaymessenger/sdk": "0.3.6-staging.20"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@
|
|
41
|
-
"@types/node": "26.
|
|
40
|
+
"@earendil-works/pi-coding-agent": "0.86.0",
|
|
41
|
+
"@types/node": "26.6.2",
|
|
42
42
|
"typescript": "7.0.2",
|
|
43
43
|
"vitest": "4.1.11"
|
|
44
44
|
},
|