pi-roundtable-line 0.16.0 → 0.17.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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.17.0] - 2026-10-16
9
+
10
+ ### Added
11
+
12
+ - The `line` surface tells the model that LINE shows plain text only: its `formatting` text is appended to the system prompt of every turn that replies in a LINE chat, after the persona. `line({ formatting })` replaces it and `false` tells the model nothing. Needs pi-roundtable with `ChatSurface.formatting`.
13
+ - `LINE_FORMATTING`: the built-in guidance text, for a host that wants the same words elsewhere, such as in the description of a tool that sends to LINE.
14
+ - Release in lockstep with pi-roundtable 0.17.0. The core peer range moves to `>=0.17.0 <0.18.0`.
15
+
8
16
  ## [0.16.0] - 2026-10-16
9
17
 
10
18
  ### Added
package/README.md CHANGED
@@ -390,6 +390,10 @@ What the host writes there (`sendReply`) is queued as text objects, one per chun
390
390
  The surface shows no typing indicator, stop control, progress, files or approval prompts: LINE has none the bot can show.
391
391
  It passes `describeSurfaceContract` (`pi-roundtable/testing`).
392
392
 
393
+ LINE shows plain text only, so the surface tells the model so: its `formatting` text is appended to the system prompt of every turn that replies in a LINE chat, after the persona, and says to use no headings, tables, bold, code fences or link syntax and to keep replies short.
394
+ `formatting: "…"` in the plugin options replaces the text, and `formatting: false` tells the model nothing.
395
+ The built-in text is exported as `LINE_FORMATTING`, so a host can reuse it, such as in the description of a tool that sends to LINE.
396
+
393
397
  ## The sender icon route
394
398
 
395
399
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable-line",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "A LINE Messaging API transport for pi-roundtable: a signed webhook that persists events before it answers, a durable inbox worker, and an allowlist of the groups and chats it mirrors",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,14 +33,14 @@
33
33
  "test": "bun test"
34
34
  },
35
35
  "peerDependencies": {
36
- "pi-roundtable": ">=0.16.0 <0.17.0"
36
+ "pi-roundtable": ">=0.17.0 <0.18.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@biomejs/biome": "2.5.15",
40
40
  "@earendil-works/pi-ai": ">=1.0.0 <2",
41
41
  "@earendil-works/pi-coding-agent": ">=1.0.0 <2",
42
42
  "@types/bun": "1.4.2",
43
- "pi-roundtable": "0.16.0",
43
+ "pi-roundtable": "0.17.0",
44
44
  "typebox": "1.3.34",
45
45
  "typescript": "7.0.2"
46
46
  }
package/src/index.ts CHANGED
@@ -67,7 +67,7 @@ export type {
67
67
  LineSources,
68
68
  SourceStatus,
69
69
  } from "./sources.ts";
70
- export { LineSurface } from "./surface.ts";
70
+ export { LINE_FORMATTING, LineSurface } from "./surface.ts";
71
71
  export type { LineCredentials, TokenMode } from "./token.ts";
72
72
  export type { WebhookHealth } from "./webhook.ts";
73
73
  export { WEBHOOK_MAX_BYTES } from "./webhook.ts";
package/src/options.ts CHANGED
@@ -38,6 +38,13 @@ export interface LineOptions {
38
38
  * through `everyone`.
39
39
  */
40
40
  route?: boolean;
41
+ /**
42
+ * What the model is told, English, about how a reply shows in LINE, appended to the system
43
+ * prompt of every turn that replies there, after the persona. The default says LINE shows plain
44
+ * text only, so Markdown appears as literal symbols. A text replaces it; `false` tells the
45
+ * model nothing.
46
+ */
47
+ formatting?: string | false;
41
48
  /** Serves one fixed PNG at `path`, so a sender can name it as its icon without another service. */
42
49
  senderIconRoute?: SenderIconRoute;
43
50
  /** Replaces `fetch` for every call to LINE, for tests. */
@@ -83,6 +90,13 @@ export function checkOptions(options: LineOptions): {
83
90
  throw new Error(
84
91
  `line: webhookPath ${JSON.stringify(path)} must start with / and not end with one, such as /line/webhook`,
85
92
  );
93
+ if (
94
+ typeof options.formatting === "string" &&
95
+ options.formatting.trim() === ""
96
+ )
97
+ throw new Error(
98
+ "line: formatting is empty; write the guidance, or false to give the model none",
99
+ );
86
100
  for (const [key, value] of Object.entries(options.inbox ?? {})) {
87
101
  if (!INBOX_KEYS.includes(key as keyof InboxOptions))
88
102
  throw new Error(`line: inbox.${key} is not an inbox option`);
package/src/plugin.ts CHANGED
@@ -68,7 +68,7 @@ export function line(options: LineOptions): RoundtablePlugin {
68
68
  ...(options.sender ? { sender: options.sender } : {}),
69
69
  queued: (readyInMs) => outboxWorker?.kickIn(readyInMs),
70
70
  });
71
- const surface = new LineSurface(outbox);
71
+ const surface = new LineSurface(outbox, options.formatting);
72
72
  const joins = new JoinNotifier({
73
73
  sql,
74
74
  api,
package/src/surface.ts CHANGED
@@ -13,6 +13,14 @@ import type { LineOutbox, LineOutMessage } from "./outbox.ts";
13
13
  /** The longest text one LINE message holds. */
14
14
  const TEXT_MAX = 5000;
15
15
 
16
+ /**
17
+ * What the model is told about LINE's rendering, as the surface's `formatting` unless the host
18
+ * replaces it; exported so a host can reuse it, such as in the description of a tool that sends to
19
+ * LINE. English: it is model-facing text, not a message-catalog string.
20
+ */
21
+ export const LINE_FORMATTING =
22
+ "Reply formatting (LINE): plain text only. Markdown is not rendered and shows as literal symbols, so use no headings, tables, bold, code fences or link syntax. Write short paragraphs, and lists with plain dashes or numbers. Keep replies short.";
23
+
16
24
  /** `text` cut into pieces LINE takes, never inside a surrogate pair. */
17
25
  function pieces(text: string): string[] {
18
26
  const parts: string[] = [];
@@ -39,11 +47,21 @@ function pieces(text: string): string[] {
39
47
  */
40
48
  export class LineSurface implements ChatSurface {
41
49
  readonly surface = "line";
50
+ readonly formatting: string | undefined;
42
51
  readonly #outbox: LineOutbox;
43
52
  #deliver: ((message: InboundMessage) => void) | undefined;
44
53
 
45
- constructor(outbox: LineOutbox) {
54
+ /** `formatting` replaces what the model is told about LINE's plain text, and `false` tells it nothing. */
55
+ constructor(outbox: LineOutbox, formatting?: string | false) {
46
56
  this.#outbox = outbox;
57
+ if (formatting === false) this.formatting = undefined;
58
+ else if (formatting === undefined) this.formatting = LINE_FORMATTING;
59
+ else if (formatting.trim() === "")
60
+ throw new LineError(
61
+ "formatting_empty",
62
+ "line: formatting is empty; write the guidance, or false to give the model none",
63
+ );
64
+ else this.formatting = formatting;
47
65
  }
48
66
 
49
67
  async start(deliver: (message: InboundMessage) => void): Promise<void> {