@rse/ase 0.0.27 → 0.0.28

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.
@@ -6,6 +6,7 @@
6
6
  import fs from "node:fs";
7
7
  import { InvalidArgumentError } from "commander";
8
8
  import { renderMermaidASCII } from "beautiful-mermaid";
9
+ import { z } from "zod";
9
10
  /* custom argument parser for Commander: non-negative integer */
10
11
  const parseInteger = (name) => (value) => {
11
12
  const n = Number.parseInt(value, 10);
@@ -19,57 +20,6 @@ const parseColorMode = (name) => (value) => {
19
20
  throw new InvalidArgumentError(`${name} must be "none", "ansi16", or "ansi256"`);
20
21
  return value;
21
22
  };
22
- /* detect terminal column width */
23
- export const detectTermWidth = () => {
24
- let width = 0;
25
- /* attempt 1: query environment variable */
26
- if (process.env.ASE_TERM_WIDTH !== undefined) {
27
- const cols = Number.parseInt(process.env.ASE_TERM_WIDTH, 10);
28
- if (Number.isFinite(cols) && cols > 0)
29
- width = cols;
30
- }
31
- /* attempt 2: query stdout */
32
- if (width === 0 && process.stdout.isTTY) {
33
- const cols = process.stdout.columns;
34
- if (typeof cols === "number" && cols > 0)
35
- width = cols;
36
- }
37
- return width;
38
- };
39
- /* detect terminal row height */
40
- export const detectTermHeight = () => {
41
- let height = 0;
42
- /* attempt 1: query environment variable */
43
- if (process.env.ASE_TERM_HEIGHT !== undefined) {
44
- const rows = Number.parseInt(process.env.ASE_TERM_HEIGHT, 10);
45
- if (Number.isFinite(rows) && rows > 0)
46
- height = rows;
47
- }
48
- /* attempt 2: query stdout */
49
- if (height === 0 && process.stdout.isTTY) {
50
- const rows = process.stdout.rows;
51
- if (typeof rows === "number" && rows > 0)
52
- height = rows;
53
- }
54
- return height;
55
- };
56
- /* detect terminal color capability */
57
- const detectColorMode = () => {
58
- let mode = "none";
59
- /* attempt 1: query environment variable (explicitly) */
60
- if (process.env.ASE_TERM_COLORS !== undefined)
61
- if (process.env.ASE_TERM_COLORS.match(/^(?:none|ansi16|ansi256)$/) !== null)
62
- mode = process.env.ASE_TERM_COLORS;
63
- /* attempt 2: query stdout */
64
- if (mode === "none" && process.stdout.isTTY) {
65
- const depth = process.stdout.getColorDepth();
66
- if (depth >= 8)
67
- mode = "ansi256";
68
- else if (depth >= 4)
69
- mode = "ansi16";
70
- }
71
- return mode;
72
- };
73
23
  /* truncate a single rendered line to a maximum visible column,
74
24
  preserving ANSI escape sequences (CSI ...m) and appending an ANSI
75
25
  reset sequence if any styling was active at the truncation point */
@@ -135,70 +85,124 @@ const visibleWidth = (line) => {
135
85
  }
136
86
  return visible;
137
87
  };
138
- /* pure rendering helper: turn a Mermaid source string plus options into
139
- a rendered Unicode/ASCII diagram string. Throws on render failure. */
140
- export const renderDiagram = (src, opts) => {
141
- /* create diagram rendering */
142
- let out = renderMermaidASCII(src, {
143
- useAscii: opts.ascii,
144
- paddingX: opts.nodeMarginX,
145
- paddingY: opts.nodeMarginY,
146
- boxBorderPadding: opts.nodePadding,
147
- colorMode: opts.colorMode,
148
- theme: opts.colorMode !== "none" ? {
149
- fg: "#000000",
150
- border: "#a0a0a0",
151
- junction: "#a0a0a0",
152
- arrow: "#404040",
153
- line: "#707070",
154
- corner: "#707070"
155
- } : {
156
- fg: "#000000",
157
- border: "#000000",
158
- junction: "#000000",
159
- arrow: "#000000",
160
- line: "#000000",
161
- corner: "#000000"
88
+ /* reusable functionality: Mermaid diagram rendering as Unicode/ASCII art */
89
+ export class Diagram {
90
+ /* detect terminal column width */
91
+ static detectTermWidth() {
92
+ let width = 0;
93
+ /* attempt 1: query environment variable */
94
+ if (process.env.ASE_TERM_WIDTH !== undefined) {
95
+ const cols = Number.parseInt(process.env.ASE_TERM_WIDTH, 10);
96
+ if (Number.isFinite(cols) && cols > 0)
97
+ width = cols;
162
98
  }
163
- });
164
- /* optionally clip diagram rendering */
165
- const termWidth = opts.terminalWidth;
166
- const termHeight = opts.terminalHeight;
167
- if (termWidth > 0 || termHeight > 0) {
168
- const maxWidth = termWidth > 0 ? termWidth - opts.diagramClipX : 0;
169
- const maxHeight = termHeight > 0 ? termHeight - opts.diagramClipY : 0;
170
- const trailingNL = out.endsWith("\n");
171
- let lines = (trailingNL ? out.slice(0, -1) : out).split("\n");
172
- let widthWarn = "";
173
- let heightWarn = "";
174
- if (maxWidth > 0) {
175
- const widest = lines.reduce((m, l) => Math.max(m, visibleWidth(l)), 0);
176
- if (widest > maxWidth)
177
- widthWarn =
178
- `ase diagram: WARNING: rendered diagram width ${widest} exceeds budget ${maxWidth}; ` +
179
- "rightmost content was clipped. Please regenerate the Mermaid source to fit " +
180
- `within ${maxWidth} chars by preferring a portrait orientation ` +
181
- "(\"flowchart TB\", top-to-bottom) over landscape (\"LR\"/\"RL\"/\"BT\"), " +
182
- "reducing siblings per row, abbreviating node labels, or restructuring " +
183
- "into nested subgraph hierarchies.";
184
- lines = lines.map((l) => truncateAnsiLine(l, maxWidth));
99
+ /* attempt 2: query stdout */
100
+ if (width === 0 && process.stdout.isTTY) {
101
+ const cols = process.stdout.columns;
102
+ if (typeof cols === "number" && cols > 0)
103
+ width = cols;
185
104
  }
186
- if (maxHeight > 0 && lines.length > maxHeight) {
187
- const overflow = lines.length - maxHeight;
188
- heightWarn =
189
- `ase diagram: WARNING: rendered diagram height ${lines.length} exceeds budget ${maxHeight}; ` +
190
- `bottom ${overflow} line(s) were clipped. Please regenerate the Mermaid source to fit ` +
191
- `within ${maxHeight} lines by reducing depth or splitting into multiple diagrams.`;
192
- lines = lines.slice(0, maxHeight);
105
+ return width;
106
+ }
107
+ /* detect terminal row height */
108
+ static detectTermHeight() {
109
+ let height = 0;
110
+ /* attempt 1: query environment variable */
111
+ if (process.env.ASE_TERM_HEIGHT !== undefined) {
112
+ const rows = Number.parseInt(process.env.ASE_TERM_HEIGHT, 10);
113
+ if (Number.isFinite(rows) && rows > 0)
114
+ height = rows;
115
+ }
116
+ /* attempt 2: query stdout */
117
+ if (height === 0 && process.stdout.isTTY) {
118
+ const rows = process.stdout.rows;
119
+ if (typeof rows === "number" && rows > 0)
120
+ height = rows;
193
121
  }
194
- out = lines.join("\n") + (trailingNL ? "\n" : "");
195
- if (widthWarn !== "")
196
- out += "\n" + widthWarn + "\n";
197
- if (heightWarn !== "")
198
- out += "\n" + heightWarn + "\n";
122
+ return height;
199
123
  }
200
- return out;
201
- };
124
+ /* detect terminal color capability */
125
+ static detectColorMode() {
126
+ let mode = "none";
127
+ /* attempt 1: query environment variable (explicitly) */
128
+ if (process.env.ASE_TERM_COLORS !== undefined)
129
+ if (process.env.ASE_TERM_COLORS.match(/^(?:none|ansi16|ansi256)$/) !== null)
130
+ mode = process.env.ASE_TERM_COLORS;
131
+ /* attempt 2: query stdout */
132
+ if (mode === "none" && process.stdout.isTTY) {
133
+ const depth = process.stdout.getColorDepth();
134
+ if (depth >= 8)
135
+ mode = "ansi256";
136
+ else if (depth >= 4)
137
+ mode = "ansi16";
138
+ }
139
+ return mode;
140
+ }
141
+ /* pure rendering helper: turn a Mermaid source string plus options into
142
+ a rendered Unicode/ASCII diagram string. Throws on render failure. */
143
+ static render(src, opts) {
144
+ /* create diagram rendering */
145
+ let out = renderMermaidASCII(src, {
146
+ useAscii: opts.ascii,
147
+ paddingX: opts.nodeMarginX,
148
+ paddingY: opts.nodeMarginY,
149
+ boxBorderPadding: opts.nodePadding,
150
+ colorMode: opts.colorMode,
151
+ theme: opts.colorMode !== "none" ? {
152
+ fg: "#000000",
153
+ border: "#a0a0a0",
154
+ junction: "#a0a0a0",
155
+ arrow: "#404040",
156
+ line: "#707070",
157
+ corner: "#707070"
158
+ } : {
159
+ fg: "#000000",
160
+ border: "#000000",
161
+ junction: "#000000",
162
+ arrow: "#000000",
163
+ line: "#000000",
164
+ corner: "#000000"
165
+ }
166
+ });
167
+ /* optionally clip diagram rendering */
168
+ const termWidth = opts.terminalWidth;
169
+ const termHeight = opts.terminalHeight;
170
+ if (termWidth > 0 || termHeight > 0) {
171
+ const maxWidth = termWidth > 0 ? termWidth - opts.diagramClipX : 0;
172
+ const maxHeight = termHeight > 0 ? termHeight - opts.diagramClipY : 0;
173
+ const trailingNL = out.endsWith("\n");
174
+ let lines = (trailingNL ? out.slice(0, -1) : out).split("\n");
175
+ let widthWarn = "";
176
+ let heightWarn = "";
177
+ if (maxWidth > 0) {
178
+ const widest = lines.reduce((m, l) => Math.max(m, visibleWidth(l)), 0);
179
+ if (widest > maxWidth)
180
+ widthWarn =
181
+ `ase diagram: WARNING: rendered diagram width ${widest} exceeds budget ${maxWidth}; ` +
182
+ "rightmost content was clipped. Please regenerate the Mermaid source to fit " +
183
+ `within ${maxWidth} chars by preferring a portrait orientation ` +
184
+ "(\"flowchart TB\", top-to-bottom) over landscape (\"LR\"/\"RL\"/\"BT\"), " +
185
+ "reducing siblings per row, abbreviating node labels, or restructuring " +
186
+ "into nested subgraph hierarchies.";
187
+ lines = lines.map((l) => truncateAnsiLine(l, maxWidth));
188
+ }
189
+ if (maxHeight > 0 && lines.length > maxHeight) {
190
+ const overflow = lines.length - maxHeight;
191
+ heightWarn =
192
+ `ase diagram: WARNING: rendered diagram height ${lines.length} exceeds budget ${maxHeight}; ` +
193
+ `bottom ${overflow} line(s) were clipped. Please regenerate the Mermaid source to fit ` +
194
+ `within ${maxHeight} lines by reducing depth or splitting into multiple diagrams.`;
195
+ lines = lines.slice(0, maxHeight);
196
+ }
197
+ out = lines.join("\n") + (trailingNL ? "\n" : "");
198
+ if (widthWarn !== "")
199
+ out += "\n" + widthWarn + "\n";
200
+ if (heightWarn !== "")
201
+ out += "\n" + heightWarn + "\n";
202
+ }
203
+ return out;
204
+ }
205
+ }
202
206
  /* read stdin into a single string */
203
207
  const readStdin = async () => {
204
208
  const chunks = [];
@@ -219,14 +223,14 @@ export default class DiagramCommand {
219
223
  .description("Render Mermaid diagram specification as Unicode/ASCII art")
220
224
  .option("-i, --input <file>", "read Mermaid source from file instead of stdin")
221
225
  .option("-a, --ascii", "emit plain ASCII (+-|) instead of Unicode box-drawing", false)
222
- .option("-c, --color-mode <mode>", "force color mode (\"none\", \"ansi16\", or \"ansi256\")", parseColorMode("--color-mode"), detectColorMode())
226
+ .option("-c, --color-mode <mode>", "force color mode (\"none\", \"ansi16\", or \"ansi256\")", parseColorMode("--color-mode"), Diagram.detectColorMode())
223
227
  .option("--node-margin-x <n>", "horizontal margin between nodes of <n> characters", parseInteger("--node-margin-x"), 3)
224
228
  .option("--node-margin-y <n>", "vertical margin between nodes of <n> lines", parseInteger("--node-margin-y"), 3)
225
229
  .option("--node-padding <n>", "horizontal and vertical inner node padding with <n> characters", parseInteger("--node-padding"), 1)
226
230
  .option("--diagram-clip-x <n>", "extra horizontal clipping of diagram to terminal width minus <n> characters", parseInteger("--diagram-clip-x"), 0)
227
231
  .option("--diagram-clip-y <n>", "extra vertical clipping of diagram to terminal height minus <n> lines", parseInteger("--diagram-clip-y"), 0)
228
- .option("--terminal-width <n>", "width of terminal of <n> characters (for diagram clipping)", parseInteger("--terminal-width"), detectTermWidth())
229
- .option("--terminal-height <n>", "height of terminal of <n> lines (for diagram clipping)", parseInteger("--terminal-height"), detectTermHeight())
232
+ .option("--terminal-width <n>", "width of terminal of <n> characters (for diagram clipping)", parseInteger("--terminal-width"), Diagram.detectTermWidth())
233
+ .option("--terminal-height <n>", "height of terminal of <n> lines (for diagram clipping)", parseInteger("--terminal-height"), Diagram.detectTermHeight())
230
234
  .action(async (opts) => {
231
235
  /* fetch Mermaid diagram specification from stdin */
232
236
  let src;
@@ -241,7 +245,7 @@ export default class DiagramCommand {
241
245
  /* create diagram rendering */
242
246
  let out;
243
247
  try {
244
- out = renderDiagram(src, {
248
+ out = Diagram.render(src, {
245
249
  ascii: opts.ascii ?? false,
246
250
  colorMode: opts.colorMode,
247
251
  nodeMarginX: opts.nodeMarginX,
@@ -265,3 +269,68 @@ export default class DiagramCommand {
265
269
  });
266
270
  }
267
271
  }
272
+ /* MCP registration entry point for diagram tools */
273
+ export class DiagramMCP {
274
+ register(mcp) {
275
+ mcp.registerTool("diagram", {
276
+ title: "ASE diagram render",
277
+ description: "Render a Mermaid diagram as Unicode/ASCII art. " +
278
+ "Use for visualizing " +
279
+ "structure/layout/components/dependencies as a Flowchart, " +
280
+ "control-flow/branching/concurrency as a Flowchart, " +
281
+ "state-machine/states/transitions as an UML State Diagram, " +
282
+ "data-flow/actors/messages/protocols as an UML Sequence Diagram, " +
283
+ "data-structure/classes/methods as an UML Class Diagram, " +
284
+ "data-model/entities/relationships as an ER Diagram, or " +
285
+ "metrics/distributions/time-series as an XY-Chart. " +
286
+ "Pass the Mermaid diagram specification as `diagram`. " +
287
+ "Returns the rendered art as `text`.",
288
+ inputSchema: {
289
+ diagram: z.string()
290
+ .describe("Mermaid diagram specification"),
291
+ ascii: z.boolean().default(false)
292
+ .describe("emit plain ASCII (+-|) instead of Unicode box-drawing characters"),
293
+ colorMode: z.enum(["none", "ansi16", "ansi256"]).default("none")
294
+ .describe("color mode for ANSI escape sequences in the rendered output"),
295
+ nodeMarginX: z.number().int().min(0).default(3)
296
+ .describe("horizontal margin between nodes, in characters"),
297
+ nodeMarginY: z.number().int().min(0).default(3)
298
+ .describe("vertical margin between nodes, in lines"),
299
+ nodePadding: z.number().int().min(0).default(1)
300
+ .describe("inner horizontal and vertical padding within each node, in characters"),
301
+ diagramClipX: z.number().int().min(0).default(0)
302
+ .describe("extra horizontal clipping: subtract this many characters from `terminalWidth`"),
303
+ diagramClipY: z.number().int().min(0).default(0)
304
+ .describe("extra vertical clipping: subtract this many lines from `terminalHeight`"),
305
+ terminalWidth: z.number().int().min(0).default(Diagram.detectTermWidth())
306
+ .describe("terminal width in characters; 0 disables horizontal clipping; defaults to ASE_TERM_WIDTH env var if set"),
307
+ terminalHeight: z.number().int().min(0).default(Diagram.detectTermHeight())
308
+ .describe("terminal height in lines; 0 disables vertical clipping; defaults to ASE_TERM_HEIGHT env var if set")
309
+ }
310
+ }, async (args) => {
311
+ try {
312
+ const out = Diagram.render(args.diagram, {
313
+ ascii: args.ascii,
314
+ colorMode: args.colorMode,
315
+ nodeMarginX: args.nodeMarginX,
316
+ nodeMarginY: args.nodeMarginY,
317
+ nodePadding: args.nodePadding,
318
+ diagramClipX: args.diagramClipX,
319
+ diagramClipY: args.diagramClipY,
320
+ terminalWidth: args.terminalWidth,
321
+ terminalHeight: args.terminalHeight
322
+ });
323
+ return {
324
+ content: [{ type: "text", text: out }]
325
+ };
326
+ }
327
+ catch (err) {
328
+ const message = err instanceof Error ? err.message : String(err);
329
+ return {
330
+ isError: true,
331
+ content: [{ type: "text", text: `diagram: render failed: ${message}` }]
332
+ };
333
+ }
334
+ });
335
+ }
336
+ }
package/dst/ase-hook.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
+ import os from "node:os";
8
9
  import { execaSync } from "execa";
9
10
  import Version from "./ase-version.js";
10
11
  import { Config, configSchema, parseScope } from "./ase-config.js";
@@ -173,6 +174,26 @@ export default class HookCommand {
173
174
  process.stdout.write(JSON.stringify(payload));
174
175
  return 0;
175
176
  }
177
+ /* handler for "ase hook session-end" (both tools) */
178
+ doSessionEnd(_tool) {
179
+ /* read session information (Claude Code uses snake_case fields,
180
+ Copilot CLI uses camelCase fields) */
181
+ const stdin = fs.readFileSync(0, "utf8");
182
+ const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
183
+ /* determine session id */
184
+ const sessionId = input.session_id ?? input.sessionId ?? "";
185
+ /* remove the session directory ~/.ase/session/<id> (only for a valid sessionId) */
186
+ if (/^[A-Za-z0-9._-]+$/.test(sessionId)) {
187
+ const dir = path.join(os.homedir(), ".ase", "session", sessionId);
188
+ try {
189
+ fs.rmSync(dir, { recursive: true, force: true });
190
+ }
191
+ catch (_e) {
192
+ /* best-effort: ignore failures */
193
+ }
194
+ }
195
+ return 0;
196
+ }
176
197
  /* handler for "ase hook pre-tool-use" (both tools) */
177
198
  doPreToolUse(tool) {
178
199
  const spec = toolSpecs[tool];
@@ -254,6 +275,14 @@ export default class HookCommand {
254
275
  .action(async (opts) => {
255
276
  process.exit(await this.doSessionStart(this.parseTool(opts.tool)));
256
277
  });
278
+ /* register CLI sub-command "ase hook session-end" */
279
+ hookCmd
280
+ .command("session-end")
281
+ .description("handle SessionEnd hook event")
282
+ .option("-t, --tool <tool>", "target tool (\"claude\" or \"copilot\")", toolDflt)
283
+ .action((opts) => {
284
+ process.exit(this.doSessionEnd(this.parseTool(opts.tool)));
285
+ });
257
286
  /* register CLI sub-command "ase hook pre-tool-use" */
258
287
  hookCmd
259
288
  .command("pre-tool-use")
package/dst/ase-mcp.js CHANGED
@@ -9,7 +9,7 @@ import { execa } from "execa";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
11
11
  import { Config, configSchema } from "./ase-config.js";
12
- import { SERVICE_HOST as HOST, serviceSchema, probe } from "./ase-service-probe.js";
12
+ import { SERVICE_HOST as HOST, serviceSchema, probe } from "./ase-service.js";
13
13
  /* CLI command "ase mcp" */
14
14
  export default class MCPCommand {
15
15
  log;
@@ -0,0 +1,87 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+ import { isScalar } from "yaml";
7
+ import { z } from "zod";
8
+ import { Config, configSchema, parseScope } from "./ase-config.js";
9
+ /* reusable functionality: ASE agent persona style get/set */
10
+ export class Persona {
11
+ /* allowed persona style values */
12
+ static styles = ["writer", "engineer", "telegrapher", "caveman"];
13
+ /* get the effective persona style for an optional session;
14
+ returns the default "engineer" if nothing is configured */
15
+ static get(log, session) {
16
+ const scope = session !== undefined ?
17
+ parseScope(`session:${session}`) :
18
+ parseScope(undefined);
19
+ const cfg = new Config("config", configSchema, log, scope);
20
+ cfg.read();
21
+ const val = cfg.get("agent.persona");
22
+ if (val === undefined)
23
+ return "engineer";
24
+ return String(isScalar(val) ? val.value : val);
25
+ }
26
+ /* set the persona style on the strongest scope of an optional session */
27
+ static set(log, style, session) {
28
+ const scope = session !== undefined ?
29
+ parseScope(`session:${session}`) :
30
+ parseScope(undefined);
31
+ const cfg = new Config("config", configSchema, log, scope);
32
+ cfg.read();
33
+ cfg.set("agent.persona", style);
34
+ cfg.write();
35
+ }
36
+ }
37
+ /* MCP registration entry point for persona tools */
38
+ export default class PersonaMCP {
39
+ log;
40
+ constructor(log) {
41
+ this.log = log;
42
+ }
43
+ register(mcp) {
44
+ mcp.registerTool("persona", {
45
+ title: "ASE persona style get/set",
46
+ description: "Get or set the active ASE agent persona `style`. " +
47
+ "If `style` is provided, it sets the persona style, " +
48
+ "otherwise it returns the current persona `style`. " +
49
+ "If `session` is provided, the operation is scoped to that session, " +
50
+ "otherwise it operates on the broadest scope (user/project cascade). " +
51
+ "Allowed styles: \"writer\" (decorative, eloquent, explaining), " +
52
+ "\"engineer\" (brief, factual, accurate), " +
53
+ "\"telegrapher\" (very brief, factual, abbreviating), " +
54
+ "\"caveman\" (ultra brief, rough, stuttering).",
55
+ inputSchema: {
56
+ style: z.enum(Persona.styles).optional()
57
+ .describe("persona style to set; if omitted, the current persona style is returned"),
58
+ session: z.string().optional()
59
+ .describe("session identifier (allowed characters: A-Z, a-z, 0-9, '-'); " +
60
+ "if omitted, the operation is not scoped to a specific session")
61
+ }
62
+ }, async (args) => {
63
+ try {
64
+ if (args.style !== undefined) {
65
+ Persona.set(this.log, args.style, args.session);
66
+ const where = args.session !== undefined ?
67
+ ` for session "${args.session}"` : "";
68
+ const msg = `persona: OK: set agent.persona to "${args.style}"${where}`;
69
+ return {
70
+ content: [{ type: "text", text: msg }]
71
+ };
72
+ }
73
+ const text = Persona.get(this.log, args.session);
74
+ return {
75
+ content: [{ type: "text", text }]
76
+ };
77
+ }
78
+ catch (err) {
79
+ const message = err instanceof Error ? err.message : String(err);
80
+ return {
81
+ isError: true,
82
+ content: [{ type: "text", text: `persona: ERROR: ${message}` }]
83
+ };
84
+ }
85
+ });
86
+ }
87
+ }