@relaymessenger/pi 0.1.4-staging.3 → 0.1.4-staging.31

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 CHANGED
@@ -15,3 +15,25 @@ 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
+ ## Payment
34
+
35
+ The agent ends the final Pi answer with a `payment` JSON fence holding the
36
+ payment request's fields (`description`, `category`, and `amount` with
37
+ `currency`, or `mode: "subscription"` with `price_id`). The plugin creates the
38
+ request with its own Relay token, on the card's own idempotency key; the words
39
+ go first and the payment card follows as its own Message.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import Relay from "@relaymessenger/sdk";
1
+ import Relay, { type MessagePart, type PaymentRequestCreateParams } from "@relaymessenger/sdk";
2
2
  export interface PiChannelOptions {
3
3
  readonly agentToken: string;
4
4
  readonly baseURL?: string;
@@ -16,6 +16,23 @@ 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
+ payment?: PaymentRequestCreateParams;
35
+ }[];
19
36
  export declare class PiChannel {
20
37
  #private;
21
38
  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, PAYMENT_BLOCK_INSTRUCTION, PAYMENT_GUIDANCE, selectionReply, selectionReplyContext, SELECTION_GUIDANCE, SELECTION_BLOCK_INSTRUCTION, LINK_LINE_INSTRUCTION, answerMessages as splitAnswer, createPaymentPart, RelayAPIError, } 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,11 +11,48 @@ class ChildPiProcess {
11
11
  const textFromEvent = (event) => {
12
12
  if (event.event_type !== "message.received" || event.data.direction !== "inbound")
13
13
  return null;
14
- return event.data.parts
15
- .flatMap((part) => part.type === "text" || part.type === "link"
16
- ? [part.value]
17
- : part.type === "button_reply" ? [part.label] : [])
18
- .join("\n").trim() || null;
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} ${PAYMENT_BLOCK_INSTRUCTION} ${PAYMENT_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
+ // The payment request the block described: created with the card's own key
52
+ // and sent as the last Message.
53
+ if (split.payment)
54
+ messages.push({ parts: [], payment: split.payment });
55
+ return messages;
19
56
  };
20
57
  class ChatSession {
21
58
  process;
@@ -96,6 +133,10 @@ export class PiChannel {
96
133
  stop() { for (const session of this.#sessions.values())
97
134
  session.stop(); this.#sessions.clear(); }
98
135
  async #handle(event, signal) {
136
+ // Concurrent redelivery must await the original handoff, not ACK early.
137
+ const inflight = this.#inflight.get(event.event_id);
138
+ if (inflight)
139
+ return inflight;
99
140
  if (this.#seen.has(event.event_id))
100
141
  return;
101
142
  const message = textFromEvent(event);
@@ -126,7 +167,7 @@ export class PiChannel {
126
167
  this.#sessions.set(data.chat.id, session);
127
168
  }
128
169
  const timeout = this.#options.rpcTimeoutMs ?? 60_000;
129
- await session.command("prompt", { message }, timeout, signal);
170
+ await session.command("prompt", { message: piPrompt(message) }, timeout, signal);
130
171
  if (!session.settled) {
131
172
  while (!session.settled)
132
173
  await session.read(timeout, signal);
@@ -135,9 +176,25 @@ export class PiChannel {
135
176
  const answer = response.data?.text?.trim();
136
177
  if (!answer)
137
178
  throw new Error("Pi returned no final text answer");
138
- const chunks = answer.match(/[\s\S]{1,10000}/gu) ?? [];
139
- for (const [index, chunk] of chunks.entries())
140
- await this.#relay.chats.messages.send(data.chat.id, { message: { parts: [{ type: "text", value: chunk }], idempotency_key: `pi-${event.event_id}-${index}` } });
179
+ const messages = answerMessages(answer);
180
+ if (messages[0]?.error)
181
+ console.error(`Relay: the component block in pi's answer was left as text: ${messages[0].error}.`);
182
+ for (const [index, message] of messages.entries()) {
183
+ const key = `pi-${event.event_id}-${index}`;
184
+ let parts = message.parts;
185
+ if (message.payment) {
186
+ try {
187
+ parts = [await createPaymentPart(this.#relay, message.payment, key)];
188
+ }
189
+ catch (error) {
190
+ if (!(error instanceof RelayAPIError) || error.retryable)
191
+ throw error;
192
+ console.error(`Relay: the payment in pi's answer was not sent: ${error.message}`);
193
+ continue;
194
+ }
195
+ }
196
+ await this.#relay.chats.messages.send(data.chat.id, { message: { parts, idempotency_key: key } });
197
+ }
141
198
  }
142
199
  }
143
200
  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 "@mariozechner/pi-coding-agent";
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",
3
+ "version": "0.1.4-staging.31",
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.3"
37
+ "@relaymessenger/sdk": "0.3.6-staging.31"
38
38
  },
39
39
  "devDependencies": {
40
- "@mariozechner/pi-coding-agent": "0.73.1",
41
- "@types/node": "26.0.0",
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
  },