pi-roundtable-drawing 0.7.2 → 0.7.4

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.7.4] - 2026-10-03
9
+
10
+ - Add `cardPresentation.result` for the card draw result text and `mapLimits` for longer relationship-map text.
11
+
12
+ ## [0.7.3] - 2026-10-03
13
+
14
+ - Add `DrawingOptions.cardPresentation` for trusted operator card headings and reversal suffixes.
15
+
8
16
  ## [0.7.2] - 2026-10-02
9
17
 
10
18
  First release published by the lockstep workflow; no changes to the package.
package/README.md CHANGED
@@ -82,8 +82,15 @@ export default {
82
82
  | `deckDir` | `string` | none | A directory with one subdirectory for each deck; see [the deck directory](#the-deck-directory). Without it the plugin has no `draw_cards` tool. A directory that does not exist, holds no deck, or holds an invalid manifest stops the start with a message that names the deck and what to fix |
83
83
  | `random` | `() => number` | `Math.random` | The random source of the map layouts and the card draws, returning a number from 0 up to, not including, 1. A seeded source makes a run reproducible: a new `seededRandom(seed)` given the same calls draws the same pictures. `seededRandom(seed)` is exported for that |
84
84
  | `minTier` | `"owner" \| "admin" \| "member"` | `member` | The lowest tier that may use the tools. The operator's `toolTiers` setting still wins |
85
+ | `cardPresentation` | `CardPresentation` | package captions | Trusted operator-local headings, reversal suffixes and result text |
86
+ | `mapLimits` | `{ title?, text? }` | 80 and 60 | Longest relationship-map title, and node id, node label or edge label, in characters, for a host whose callers already draw longer text; the renderer's size limit still applies |
85
87
 
86
88
  The plugin is named `drawing`.
89
+ `cardPresentation.heading(draw, turn)` returns `{ title, subtitle }`; `draw` holds the selected `deck`, `count`, and optional `question`, and `turn` carries the host-bound speaker/channel.
90
+ `cardPresentation.reversedSuffix` replaces the default `" (reversed)"` on pictured reversed card names.
91
+ `cardPresentation.result({ deck, cards, positions })` replaces the text the model reads back after a draw; `{file}` stands for the attached file's name.
92
+ These hooks change only presentation, not card selection, model-visible arguments, or attachment limits.
93
+ They are trusted configuration callbacks, never guest-supplied code.
87
94
 
88
95
  ## The tools
89
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable-drawing",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Relationship maps, magic circles, sigils, sacred geometry, and card spreads drawn locally for pi-roundtable",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,7 +48,7 @@
48
48
  "devDependencies": {
49
49
  "@biomejs/biome": "2.5.15",
50
50
  "@types/bun": "1.4.2",
51
- "pi-roundtable": "0.7.2",
51
+ "pi-roundtable": "0.7.4",
52
52
  "typescript": "7.0.2"
53
53
  }
54
54
  }
@@ -112,6 +112,7 @@ export async function renderSpread(
112
112
  cards: readonly DrawnCard[],
113
113
  positions: readonly SpreadPosition[],
114
114
  heading: { title: string; subtitle: string },
115
+ reversedSuffix = " (reversed)",
115
116
  ): Promise<Uint8Array> {
116
117
  checkSpread(positions, cards.length);
117
118
  ensureFont();
@@ -225,7 +226,7 @@ export async function renderSpread(
225
226
 
226
227
  ctx.fillStyle = card.reversed ? PALETTE.accentLight : PALETTE.text;
227
228
  ctx.font = font(width < 150 ? 12 : 15);
228
- const name = card.reversed ? `${card.name} (reversed)` : card.name;
229
+ const name = card.reversed ? `${card.name}${reversedSuffix}` : card.name;
229
230
  ctx.fillText(fitText(ctx, name, width), x + width / 2, y + height + 8);
230
231
  }
231
232
  return new Uint8Array(canvas.toBuffer("image/png"));
package/src/index.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export type { Card, Deck } from "./cards/deck.ts";
2
- export type { DrawingOptions } from "./plugin.ts";
2
+ export type {
3
+ CardPresentation,
4
+ DrawingOptions,
5
+ RelationshipMapLimits,
6
+ } from "./plugin.ts";
3
7
  export { drawing } from "./plugin.ts";
4
8
  export type { Random } from "./random.ts";
5
9
  export { seededRandom } from "./random.ts";
package/src/plugin.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { definePlugin, PluginError, type Tier } from "pi-roundtable";
1
+ import {
2
+ definePlugin,
3
+ PluginError,
4
+ type Tier,
5
+ type ToolTurn,
6
+ } from "pi-roundtable";
2
7
  import { type Deck, DeckError, loadDecks } from "./cards/deck.ts";
3
8
  import type { Random } from "./random.ts";
4
9
  import { drawCardsTool } from "./tools/draw-cards-tool.ts";
@@ -10,6 +15,32 @@ import {
10
15
  } from "./tools/magic-tools.ts";
11
16
  import { relationshipMapTool } from "./tools/relationship-map-tool.ts";
12
17
 
18
+ export interface CardPresentation {
19
+ /** Trusted host presentation; never part of the model's tool arguments. */
20
+ heading?: (
21
+ draw: { deck: Deck; count: number; question?: string },
22
+ turn: ToolTurn,
23
+ ) => { title: string; subtitle: string };
24
+ /** Appended to reversed card names on the picture; default " (reversed)". */
25
+ reversedSuffix?: string;
26
+ /**
27
+ * The text the model reads back after a draw; `{file}` stands for the attached file's name.
28
+ * Default: the deck id, then one numbered line per card with its position label and id.
29
+ */
30
+ result?: (draw: {
31
+ deck: Deck;
32
+ cards: { id: string; name: string; reversed: boolean }[];
33
+ positions: { row: number; col: number; label?: string | undefined }[];
34
+ }) => string;
35
+ }
36
+
37
+ /** Longest map text a host accepts, in characters; the defaults suit a picture that stays readable. */
38
+ export interface RelationshipMapLimits {
39
+ title?: number;
40
+ /** Node ids, node labels and edge labels. */
41
+ text?: number;
42
+ }
43
+
13
44
  export interface DrawingOptions {
14
45
  /**
15
46
  * A directory with one subdirectory for each deck, each holding a `deck.json` and the card
@@ -21,6 +52,10 @@ export interface DrawingOptions {
21
52
  random?: Random;
22
53
  /** The lowest tier that may use the tools; default `member`. */
23
54
  minTier?: Tier;
55
+ /** Operator-local captions and reversal wording; default package presentation. */
56
+ cardPresentation?: CardPresentation;
57
+ /** Longer map text than the defaults, for a host whose callers already draw longer labels. */
58
+ mapLimits?: RelationshipMapLimits;
24
59
  }
25
60
 
26
61
  function readDecks(deckDir: string): Deck[] {
@@ -49,11 +84,13 @@ export function drawing(options: DrawingOptions = {}) {
49
84
  };
50
85
  return {
51
86
  tools: [
52
- relationshipMapTool(env, random),
87
+ relationshipMapTool(env, random, options.mapLimits),
53
88
  magicCircleTool(env),
54
89
  sigilTool(env),
55
90
  sacredGeometryTool(env),
56
- ...(decks.length > 0 ? [drawCardsTool(decks, env, random)] : []),
91
+ ...(decks.length > 0
92
+ ? [drawCardsTool(decks, env, random, options.cardPresentation)]
93
+ : []),
57
94
  ],
58
95
  };
59
96
  },
@@ -10,6 +10,7 @@ import {
10
10
  type SpreadPosition,
11
11
  } from "../cards/spread.ts";
12
12
  import { DrawingError } from "../errors.ts";
13
+ import type { CardPresentation } from "../plugin.ts";
13
14
  import type { Random } from "../random.ts";
14
15
  import {
15
16
  type ImageToolEnv,
@@ -32,6 +33,7 @@ export function drawCardsTool(
32
33
  decks: readonly Deck[],
33
34
  env: ImageToolEnv,
34
35
  random: Random,
36
+ presentation: CardPresentation = {},
35
37
  ) {
36
38
  const [first, ...rest] = decks;
37
39
  if (!first) throw new DrawingError("draw_cards needs at least one deck.");
@@ -81,7 +83,7 @@ export function drawCardsTool(
81
83
  Type.String({ description: "The question, shown on the picture." }),
82
84
  ),
83
85
  }),
84
- draw: async (args) => {
86
+ draw: async (args, turn) => {
85
87
  const deck = decks.find((candidate) => candidate.id === args.deck);
86
88
  if (!deck)
87
89
  throw new DrawingError(
@@ -99,12 +101,22 @@ export function drawCardsTool(
99
101
  allowReversed: args.allow_reversed,
100
102
  random,
101
103
  });
102
- const image = await renderSpread(deck, cards, positions, {
104
+ const heading = presentation.heading?.(
105
+ { deck, count: cards.length, question: args.question },
106
+ turn,
107
+ ) ?? {
103
108
  title: deck.name,
104
109
  subtitle:
105
110
  args.question ??
106
111
  `${cards.length} card${cards.length === 1 ? "" : "s"}`,
107
- });
112
+ };
113
+ const image = await renderSpread(
114
+ deck,
115
+ cards,
116
+ positions,
117
+ heading,
118
+ presentation.reversedSuffix,
119
+ );
108
120
  const lines = cards.map((card, i) => {
109
121
  const label = positions[i]?.label ? `${positions[i]?.label}: ` : "";
110
122
  return `${i + 1}. ${label}${card.name}${card.reversed ? " (reversed)" : ""} [id: ${card.id}]`;
@@ -112,7 +124,9 @@ export function drawCardsTool(
112
124
  return {
113
125
  stem: "cards",
114
126
  image,
115
- text: `Drew ${cards.length} from ${deck.id}; the spread picture is attached to your reply as {file}.\n${lines.join("\n")}`,
127
+ text:
128
+ presentation.result?.({ deck, cards, positions }) ??
129
+ `Drew ${cards.length} from ${deck.id}; the spread picture is attached to your reply as {file}.\n${lines.join("\n")}`,
116
130
  };
117
131
  },
118
132
  },
@@ -1,4 +1,5 @@
1
1
  import { Type } from "typebox";
2
+ import type { RelationshipMapLimits } from "../plugin.ts";
2
3
  import type { Random } from "../random.ts";
3
4
  import {
4
5
  EDGE_TYPES,
@@ -16,25 +17,31 @@ import {
16
17
  strictObject,
17
18
  } from "./image-tool.ts";
18
19
 
19
- export function relationshipMapTool(env: ImageToolEnv, random: Random) {
20
+ export function relationshipMapTool(
21
+ env: ImageToolEnv,
22
+ random: Random,
23
+ limits: RelationshipMapLimits = {},
24
+ ) {
25
+ const maxTitle = limits.title ?? MAX_TITLE;
26
+ const maxText = limits.text ?? MAX_TEXT;
20
27
  return imageTool(
21
28
  {
22
29
  name: "relationship_map",
23
30
  description:
24
31
  "Draw a hand-drawn relationship map of characters (pc, npc) and factions, with typed relationships between them, and attach it to your reply as an image.",
25
32
  parameters: strictObject({
26
- title: Type.Optional(Type.String({ maxLength: MAX_TITLE })),
33
+ title: Type.Optional(Type.String({ maxLength: maxTitle })),
27
34
  nodes: Type.Array(
28
35
  strictObject({
29
36
  id: Type.String({
30
37
  minLength: 1,
31
- maxLength: MAX_TEXT,
38
+ maxLength: maxText,
32
39
  description: "Character or faction name.",
33
40
  }),
34
41
  type: literals(NODE_TYPES),
35
42
  label: Type.Optional(
36
43
  Type.String({
37
- maxLength: MAX_TEXT,
44
+ maxLength: maxText,
38
45
  description: "A sub-label such as a role.",
39
46
  }),
40
47
  ),
@@ -49,7 +56,7 @@ export function relationshipMapTool(env: ImageToolEnv, random: Random) {
49
56
  EDGE_TYPES,
50
57
  "romantic: arrow; entanglement: dashed arrow; bond: plain line; faction: dashed arrow; hostile: arrows both ways.",
51
58
  ),
52
- label: Type.Optional(Type.String({ maxLength: MAX_TEXT })),
59
+ label: Type.Optional(Type.String({ maxLength: maxText })),
53
60
  }),
54
61
  { maxItems: MAX_EDGES },
55
62
  ),