@getfamiliar/shared 0.1.2

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.
Files changed (83) hide show
  1. package/build/AgentRun.d.ts +187 -0
  2. package/build/AgentRun.js +1 -0
  3. package/build/AgentRunBus.d.ts +147 -0
  4. package/build/AgentRunBus.js +362 -0
  5. package/build/Calendar.d.ts +450 -0
  6. package/build/Calendar.js +37 -0
  7. package/build/ChatMessage.d.ts +53 -0
  8. package/build/ChatMessage.js +11 -0
  9. package/build/ChatMessageBus.d.ts +110 -0
  10. package/build/ChatMessageBus.js +257 -0
  11. package/build/Config.d.ts +83 -0
  12. package/build/Config.js +1 -0
  13. package/build/ContainerTool.d.ts +34 -0
  14. package/build/ContainerTool.js +1 -0
  15. package/build/CronExpression.d.ts +53 -0
  16. package/build/CronExpression.js +77 -0
  17. package/build/Event.d.ts +280 -0
  18. package/build/Event.js +14 -0
  19. package/build/EventBus.d.ts +198 -0
  20. package/build/EventBus.js +482 -0
  21. package/build/HandlerCatalog.d.ts +75 -0
  22. package/build/HandlerCatalog.js +165 -0
  23. package/build/InferenceEvent.d.ts +30 -0
  24. package/build/InferenceEvent.js +1 -0
  25. package/build/InferenceEventBus.d.ts +39 -0
  26. package/build/InferenceEventBus.js +80 -0
  27. package/build/Mail.d.ts +250 -0
  28. package/build/Mail.js +73 -0
  29. package/build/MailStyleTemplate.d.ts +71 -0
  30. package/build/MailStyleTemplate.js +47 -0
  31. package/build/ModelMetaData.d.ts +63 -0
  32. package/build/ModelMetaData.js +12 -0
  33. package/build/PathGlob.d.ts +20 -0
  34. package/build/PathGlob.js +33 -0
  35. package/build/Plugin.d.ts +709 -0
  36. package/build/Plugin.js +29 -0
  37. package/build/PostgresConnection.d.ts +75 -0
  38. package/build/PostgresConnection.js +162 -0
  39. package/build/ScheduledSubagent.d.ts +49 -0
  40. package/build/ScheduledSubagent.js +1 -0
  41. package/build/ScheduledSubagentBus.d.ts +94 -0
  42. package/build/ScheduledSubagentBus.js +187 -0
  43. package/build/Schema.d.ts +84 -0
  44. package/build/Schema.js +570 -0
  45. package/build/StepResult.d.ts +103 -0
  46. package/build/StepResult.js +1 -0
  47. package/build/StepResultBus.d.ts +64 -0
  48. package/build/StepResultBus.js +169 -0
  49. package/build/Timezone.d.ts +58 -0
  50. package/build/Timezone.js +84 -0
  51. package/build/ToolCall.d.ts +25 -0
  52. package/build/ToolCall.js +1 -0
  53. package/build/ToolCallBus.d.ts +65 -0
  54. package/build/ToolCallBus.js +110 -0
  55. package/build/ToolLevel.d.ts +20 -0
  56. package/build/ToolLevel.js +2 -0
  57. package/build/ToolRunner.d.ts +118 -0
  58. package/build/ToolRunner.js +192 -0
  59. package/build/ToolsExpressionUtils.d.ts +80 -0
  60. package/build/ToolsExpressionUtils.js +102 -0
  61. package/build/WorkspaceFile.d.ts +104 -0
  62. package/build/WorkspaceFile.js +7 -0
  63. package/build/estimateTokens.d.ts +16 -0
  64. package/build/estimateTokens.js +18 -0
  65. package/build/index.d.ts +37 -0
  66. package/build/index.js +26 -0
  67. package/build/logging/Logger.d.ts +66 -0
  68. package/build/logging/Logger.js +90 -0
  69. package/build/markdownHtml.d.ts +14 -0
  70. package/build/markdownHtml.js +21 -0
  71. package/build/markdownTerminal.d.ts +14 -0
  72. package/build/markdownTerminal.js +376 -0
  73. package/build/testing/MockAgentRunBus.d.ts +52 -0
  74. package/build/testing/MockAgentRunBus.js +175 -0
  75. package/build/testing/MockBusStore.d.ts +22 -0
  76. package/build/testing/MockBusStore.js +22 -0
  77. package/build/testing/MockEventBus.d.ts +20 -0
  78. package/build/testing/MockEventBus.js +57 -0
  79. package/build/testing/MockLogger.d.ts +52 -0
  80. package/build/testing/MockLogger.js +65 -0
  81. package/build/testing/index.d.ts +4 -0
  82. package/build/testing/index.js +4 -0
  83. package/package.json +63 -0
@@ -0,0 +1,37 @@
1
+ export type { AgentRunCallType, AgentRunPatch, AgentRunRow, AgentRunState, NewAgentRun, } from "./AgentRun.js";
2
+ export { AgentRunBus, type AgentRunUnsubscribe } from "./AgentRunBus.js";
3
+ export { buildCalendarEventId, type CalendarApi, type CalendarAttachmentMeta, type CalendarAttendee, type CalendarChangePayload, type CalendarEventRow, type CalendarEventType, type CalendarImportance, type CalendarProvider, type CalendarResponseStatus, type CalendarRow, type CalendarSensitivity, type CalendarShowAs, type CalendarType, type CreateEventInput, type FindEventsQuery, type NewCalendarEvent, parseCalendarEventId, type UpdateEventInput, type UpsertCalendarInput, } from "./Calendar.js";
4
+ export type { ChatFilter, ChatMessage, ChatRole, NewChatMessage } from "./ChatMessage.js";
5
+ export { type ChatHandler, ChatMessageBus, type ChatUnsubscribe } from "./ChatMessageBus.js";
6
+ export type { ConfigService } from "./Config.js";
7
+ export type { ContainerToolInfo } from "./ContainerTool.js";
8
+ export { type ParsedCron, parseCron } from "./CronExpression.js";
9
+ export type { EventFile, EventFilter, EventPatch, EventRow, EventState, NewEvent, } from "./Event.js";
10
+ export { EVENT_PRIORITY } from "./Event.js";
11
+ export { DuplicateIdempotencyKeyError, EventBus } from "./EventBus.js";
12
+ export { estimateTokens } from "./estimateTokens.js";
13
+ export { HandlerCatalog, type HandlerPath } from "./HandlerCatalog.js";
14
+ export type { InferenceEventRow, InferenceOutcome, NewInferenceEvent, } from "./InferenceEvent.js";
15
+ export { InferenceEventBus } from "./InferenceEventBus.js";
16
+ export { type CreateLoggerOptions, createLogger, jsonStdoutStream, type Logger, type LogLevel, type LogStream, prettyStdoutStream, } from "./logging/Logger.js";
17
+ export { buildMailId, type ForwardInput, type MailAddress, type MailApi, type MailAttachment, type MailAttachmentMeta, type MailFolder, type MailProvider, type MailSearchHit, type MailSearchQuery, type NewMailInput, parseMailId, type ReplyInput, } from "./Mail.js";
18
+ export { MAIL_STYLE_TEMPLATE_DEFAULTS, type MailStyleTemplate, mailStyleTemplatePath, signatureToPlainText, } from "./MailStyleTemplate.js";
19
+ export { type ModelMetaData, ModelNotSupported, type ModelProviderDescriptor, } from "./ModelMetaData.js";
20
+ export { renderMarkdownToHtml } from "./markdownHtml.js";
21
+ export { renderMarkdown } from "./markdownTerminal.js";
22
+ export { matchesAnyGlob, matchesGlob } from "./PathGlob.js";
23
+ export type { AnyCommandDef, EmitHandle, EmitOptions, EventContextProvider, HostContext, McpClient, McpInfo, PluginHostManifest, PluginManifest, PluginTool, PluginToolCallContext, } from "./Plugin.js";
24
+ export { DaemonStoppedError, definePlugin } from "./Plugin.js";
25
+ export { type NotificationHandler, POSTGRES_DB, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, PostgresConnection, type PostgresConnectionConfig, } from "./PostgresConnection.js";
26
+ export type { NewScheduledSubagent, ScheduledSubagentRow } from "./ScheduledSubagent.js";
27
+ export { ScheduledSubagentBus, type ScheduledSubagentNotification, type ScheduledSubagentOp, type ScheduledSubagentUnsubscribe, } from "./ScheduledSubagentBus.js";
28
+ export { AGENTRUNS_CHANNEL, CHATMESSAGES_NEW_CHANNEL, EVENT_TERMINAL_UPDATE_SQL, EVENTS_NEW_CHANNEL, EVENTS_STATE_CHANNEL, SCHEDULED_SUBAGENTS_CHANNEL, SCHEMA_SQL, STEPRESULTS_NEW_CHANNEL, TOPIC_PATTERN, } from "./Schema.js";
29
+ export type { NewStepResult, StepResultRow } from "./StepResult.js";
30
+ export { StepResultBus, type StepResultUnsubscribe } from "./StepResultBus.js";
31
+ export { dayBoundsInZone, type ParseInZoneResult, parseInZone, renderInZone, } from "./Timezone.js";
32
+ export type { NewToolCall, ToolCallRow, ToolUsageCount } from "./ToolCall.js";
33
+ export { ToolCallBus } from "./ToolCallBus.js";
34
+ export { DEFAULT_TOOL_LEVEL, type ToolLevel } from "./ToolLevel.js";
35
+ export { DEFAULT_TOOL_CALL_OFFLOADING_LIMIT, type OffloadedJson, runJsonLinesTool, runJsonTool, runTextTool, ToolError, type ToolRunContext, truncateUtf8, } from "./ToolRunner.js";
36
+ export { ALL_GROUP_NAME, CORE_PLUGIN_ID, IDENT_PATTERN, MCP_GROUP_NAME, matchesAnyToolPattern, NONE_GROUP_NAME, RESERVED_GROUP_NAMES, toolPatternMatches, validateGroupName, } from "./ToolsExpressionUtils.js";
37
+ export type { WorkspaceFile, WorkspaceFileFilter, WorkspaceWatcherApi, } from "./WorkspaceFile.js";
package/build/index.js ADDED
@@ -0,0 +1,26 @@
1
+ export { AgentRunBus } from "./AgentRunBus.js";
2
+ export { buildCalendarEventId, parseCalendarEventId, } from "./Calendar.js";
3
+ export { ChatMessageBus } from "./ChatMessageBus.js";
4
+ export { parseCron } from "./CronExpression.js";
5
+ export { EVENT_PRIORITY } from "./Event.js";
6
+ export { DuplicateIdempotencyKeyError, EventBus } from "./EventBus.js";
7
+ export { estimateTokens } from "./estimateTokens.js";
8
+ export { HandlerCatalog } from "./HandlerCatalog.js";
9
+ export { InferenceEventBus } from "./InferenceEventBus.js";
10
+ export { createLogger, jsonStdoutStream, prettyStdoutStream, } from "./logging/Logger.js";
11
+ export { buildMailId, parseMailId, } from "./Mail.js";
12
+ export { MAIL_STYLE_TEMPLATE_DEFAULTS, mailStyleTemplatePath, signatureToPlainText, } from "./MailStyleTemplate.js";
13
+ export { ModelNotSupported, } from "./ModelMetaData.js";
14
+ export { renderMarkdownToHtml } from "./markdownHtml.js";
15
+ export { renderMarkdown } from "./markdownTerminal.js";
16
+ export { matchesAnyGlob, matchesGlob } from "./PathGlob.js";
17
+ export { DaemonStoppedError, definePlugin } from "./Plugin.js";
18
+ export { POSTGRES_DB, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, PostgresConnection, } from "./PostgresConnection.js";
19
+ export { ScheduledSubagentBus, } from "./ScheduledSubagentBus.js";
20
+ export { AGENTRUNS_CHANNEL, CHATMESSAGES_NEW_CHANNEL, EVENT_TERMINAL_UPDATE_SQL, EVENTS_NEW_CHANNEL, EVENTS_STATE_CHANNEL, SCHEDULED_SUBAGENTS_CHANNEL, SCHEMA_SQL, STEPRESULTS_NEW_CHANNEL, TOPIC_PATTERN, } from "./Schema.js";
21
+ export { StepResultBus } from "./StepResultBus.js";
22
+ export { dayBoundsInZone, parseInZone, renderInZone, } from "./Timezone.js";
23
+ export { ToolCallBus } from "./ToolCallBus.js";
24
+ export { DEFAULT_TOOL_LEVEL } from "./ToolLevel.js";
25
+ export { DEFAULT_TOOL_CALL_OFFLOADING_LIMIT, runJsonLinesTool, runJsonTool, runTextTool, ToolError, truncateUtf8, } from "./ToolRunner.js";
26
+ export { ALL_GROUP_NAME, CORE_PLUGIN_ID, IDENT_PATTERN, MCP_GROUP_NAME, matchesAnyToolPattern, NONE_GROUP_NAME, RESERVED_GROUP_NAMES, toolPatternMatches, validateGroupName, } from "./ToolsExpressionUtils.js";
@@ -0,0 +1,66 @@
1
+ import { type DestinationStream } from "pino";
2
+ /**
3
+ * Structured logger used by both host and container.
4
+ *
5
+ * Thin shape over `pino.Logger` so call sites only see the small surface
6
+ * we actually use, and so the streams plumbing (multistream, levels) is
7
+ * owned in one place. Always create child loggers via {@link Logger.child}
8
+ * so per-component tags (`component`, `source`, …) accumulate.
9
+ */
10
+ export interface Logger {
11
+ debug(obj: object, msg?: string): void;
12
+ debug(msg: string): void;
13
+ info(obj: object, msg?: string): void;
14
+ info(msg: string): void;
15
+ warn(obj: object, msg?: string): void;
16
+ warn(msg: string): void;
17
+ error(obj: object, msg?: string): void;
18
+ error(msg: string): void;
19
+ /**
20
+ * Return a child logger that always includes `bindings` on every
21
+ * record. Children share the underlying streams and level.
22
+ */
23
+ child(bindings: Record<string, unknown>): Logger;
24
+ }
25
+ /** Levels exposed to call sites. `debug` is what `--verbose` enables. */
26
+ export type LogLevel = "debug" | "info" | "warn" | "error";
27
+ /**
28
+ * One sink for the logger. Wraps a pino-compatible {@link DestinationStream}
29
+ * so {@link createLogger} can compose multiple sinks via `pino.multistream`.
30
+ */
31
+ export interface LogStream {
32
+ readonly stream: DestinationStream;
33
+ }
34
+ /** Options accepted by {@link createLogger}. */
35
+ export interface CreateLoggerOptions {
36
+ /** Top-level component tag stamped on every record. */
37
+ readonly component: string;
38
+ /** Minimum level emitted. */
39
+ readonly level: LogLevel;
40
+ /**
41
+ * One or more sinks. Multiple streams use `pino.multistream`. A
42
+ * single stream is wired directly. Levels on individual streams
43
+ * default to the logger's level.
44
+ */
45
+ readonly streams: readonly LogStream[];
46
+ }
47
+ /**
48
+ * Build a {@link Logger}. Call once per process (host: in `Start.ts`;
49
+ * container: in `index.ts`); pass children into subsystems.
50
+ *
51
+ * @throws If `streams` is empty — there's no useful default sink.
52
+ */
53
+ export declare function createLogger(options: CreateLoggerOptions): Logger;
54
+ /**
55
+ * Pretty-printed stdout for interactive use. Imports `pino-pretty`
56
+ * lazily so the container image — which never wants pretty output —
57
+ * doesn't have to ship the dep.
58
+ *
59
+ * @throws If `pino-pretty` isn't installed in the current package.
60
+ */
61
+ export declare function prettyStdoutStream(): LogStream;
62
+ /**
63
+ * Raw JSON to stdout. Used by the container (the host parses each line
64
+ * back) and by the host when stdout isn't a TTY.
65
+ */
66
+ export declare function jsonStdoutStream(): LogStream;
@@ -0,0 +1,90 @@
1
+ import { createRequire } from "node:module";
2
+ import pino, { multistream, } from "pino";
3
+ /**
4
+ * `require` shim for the ESM build. {@link prettyStdoutStream} pulls
5
+ * in `pino-pretty` lazily at call time so consumers (notably the
6
+ * container) that never use pretty output don't have to ship the dep,
7
+ * and `pino-pretty` is published as CommonJS — both reasons make a
8
+ * dynamic ESM `import()` overkill compared with `createRequire`.
9
+ */
10
+ const require = createRequire(import.meta.url);
11
+ /**
12
+ * Build a {@link Logger}. Call once per process (host: in `Start.ts`;
13
+ * container: in `index.ts`); pass children into subsystems.
14
+ *
15
+ * @throws If `streams` is empty — there's no useful default sink.
16
+ */
17
+ export function createLogger(options) {
18
+ if (options.streams.length === 0) {
19
+ throw new Error("createLogger requires at least one stream");
20
+ }
21
+ const opts = {
22
+ level: options.level,
23
+ base: { component: options.component },
24
+ timestamp: pino.stdTimeFunctions.isoTime,
25
+ };
26
+ // pino.multistream defaults each stream's accept level to `info`
27
+ // regardless of the parent logger's level, so debug records would
28
+ // be silently dropped at the stream gate. Tag every entry with the
29
+ // configured level so all streams accept it.
30
+ const pinoLogger = options.streams.length === 1
31
+ ? pino(opts, options.streams[0].stream)
32
+ : pino(opts, multistream(options.streams.map((s) => ({ stream: s.stream, level: options.level })), { dedupe: false }));
33
+ return wrap(pinoLogger);
34
+ }
35
+ /**
36
+ * Pretty-printed stdout for interactive use. Imports `pino-pretty`
37
+ * lazily so the container image — which never wants pretty output —
38
+ * doesn't have to ship the dep.
39
+ *
40
+ * @throws If `pino-pretty` isn't installed in the current package.
41
+ */
42
+ export function prettyStdoutStream() {
43
+ // Required at call time so callers that don't use it don't pay
44
+ // the resolution cost (and the container build doesn't break).
45
+ const pretty = require("pino-pretty");
46
+ const stream = pretty({
47
+ colorize: true,
48
+ translateTime: "HH:MM:ss",
49
+ ignore: "pid,hostname",
50
+ messageFormat: "[{component}{if source}/{source}{end}] {msg}",
51
+ // Suppress the trailing structured payload on stdout. Most
52
+ // call sites bake the data they want a human to see into
53
+ // the message itself (`postgres ready on 127.0.0.1:5432`,
54
+ // not `{host:..., port:...} postgres ready`); the rolling
55
+ // file sink still gets full JSON for grep / jq, so nothing
56
+ // is actually lost. See `commands/Start.ts` and friends for
57
+ // the conversion pattern.
58
+ hideObject: true,
59
+ });
60
+ return { stream };
61
+ }
62
+ /**
63
+ * Raw JSON to stdout. Used by the container (the host parses each line
64
+ * back) and by the host when stdout isn't a TTY.
65
+ */
66
+ export function jsonStdoutStream() {
67
+ return { stream: process.stdout };
68
+ }
69
+ /** Wrap a pino instance behind the {@link Logger} surface. */
70
+ function wrap(p) {
71
+ return {
72
+ debug: (a, b) => emit(p, "debug", a, b),
73
+ info: (a, b) => emit(p, "info", a, b),
74
+ warn: (a, b) => emit(p, "warn", a, b),
75
+ error: (a, b) => emit(p, "error", a, b),
76
+ child: (bindings) => wrap(p.child(bindings)),
77
+ };
78
+ }
79
+ /** Forward to pino's overloaded level method preserving the (obj, msg) form. */
80
+ function emit(p, level, a, b) {
81
+ if (typeof a === "string") {
82
+ p[level](a);
83
+ return;
84
+ }
85
+ if (b === undefined) {
86
+ p[level](a);
87
+ return;
88
+ }
89
+ p[level](a, b);
90
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Render the agent-supplied markdown body to an HTML fragment suitable for
3
+ * providers that want HTML on the wire (Microsoft Graph mail/calendar bodies,
4
+ * future SMTP/IMAP plugins, etc.). Output is a plain fragment — no `<html>`,
5
+ * `<head>`, or inline CSS — so callers can wrap it however the destination
6
+ * needs.
7
+ *
8
+ * `marked` is configured for GFM line breaks (a single newline becomes a
9
+ * `<br>`), which matches how a person typing the body in chat naturally
10
+ * thinks about paragraph breaks. The default `marked` behaviour requires two
11
+ * newlines for a paragraph break — that's markdown-correct but surprises
12
+ * agents and humans often enough to be worth the override.
13
+ */
14
+ export declare function renderMarkdownToHtml(markdownBody: string): string;
@@ -0,0 +1,21 @@
1
+ import { marked } from "marked";
2
+ /**
3
+ * Render the agent-supplied markdown body to an HTML fragment suitable for
4
+ * providers that want HTML on the wire (Microsoft Graph mail/calendar bodies,
5
+ * future SMTP/IMAP plugins, etc.). Output is a plain fragment — no `<html>`,
6
+ * `<head>`, or inline CSS — so callers can wrap it however the destination
7
+ * needs.
8
+ *
9
+ * `marked` is configured for GFM line breaks (a single newline becomes a
10
+ * `<br>`), which matches how a person typing the body in chat naturally
11
+ * thinks about paragraph breaks. The default `marked` behaviour requires two
12
+ * newlines for a paragraph break — that's markdown-correct but surprises
13
+ * agents and humans often enough to be worth the override.
14
+ */
15
+ export function renderMarkdownToHtml(markdownBody) {
16
+ return marked.parse(markdownBody, {
17
+ async: false,
18
+ gfm: true,
19
+ breaks: true,
20
+ });
21
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Render markdown to ANSI-styled terminal output at the current terminal
3
+ * width. Caller is expected to have decided (e.g. via a TTY check) that
4
+ * ANSI styling is appropriate; this function does no TTY detection of its
5
+ * own and always emits styled output.
6
+ *
7
+ * Trailing newlines are preserved — call sites that print a separator
8
+ * afterwards may want to `.replace(/\n+$/, "")` to avoid compounding
9
+ * blank lines.
10
+ *
11
+ * @param input - markdown source
12
+ * @returns ANSI-styled terminal output
13
+ */
14
+ export declare function renderMarkdown(input: string): string;
@@ -0,0 +1,376 @@
1
+ import chalk from "chalk";
2
+ import { highlight } from "cli-highlight";
3
+ import Table from "cli-table3";
4
+ import { Marked } from "marked";
5
+ import { stdout as terminalSupportsHyperlinks } from "supports-hyperlinks";
6
+ import wrapAnsi from "wrap-ansi";
7
+ /**
8
+ * Render a markdown string to ANSI-styled terminal output. Used by
9
+ * host-side `report` rendering and by the cli-chat plugin so chat
10
+ * replies pick up headings, lists, code blocks, and inline emphasis
11
+ * instead of leaking raw markdown.
12
+ *
13
+ * This is a self-contained renderer built on `marked` (the parser)
14
+ * plus `cli-highlight` (code), `cli-table3` (tables), `chalk` (inline
15
+ * styling), and `wrap-ansi` (wrapping). It replaces `marked-terminal`,
16
+ * whose block-styling-plus-reflow model produced two classes of bug we
17
+ * could not fix from the outside:
18
+ *
19
+ * 1. **Indentation-blind width.** It reflowed each block to the full
20
+ * width and only then indented it for blockquotes/lists, so nested
21
+ * prose and tables overflowed the terminal.
22
+ * 2. **ANSI bleed.** It wrapped each block in a single open/close
23
+ * style pair, then split the block into lines without re-opening
24
+ * the style per line — so colour state leaked across newlines and
25
+ * inner resets (a codespan's `ESC[39m`) cancelled the surrounding
26
+ * block colour.
27
+ *
28
+ * Both are avoided here by (a) tracking the remaining content width
29
+ * (`avail`) as we descend into containers, so every block wraps to the
30
+ * width it will actually occupy, and (b) wrapping with `wrap-ansi`,
31
+ * which emits self-contained lines (styles re-opened at each line start
32
+ * and closed at each line end). Inline spans are never wrapped — all
33
+ * wrapping happens at block boundaries — so an escape sequence is never
34
+ * split mid-line.
35
+ *
36
+ * The output is width-aware per call: every render reads the current
37
+ * terminal width, so a long-lived chat REPL picks up resizes between
38
+ * turns.
39
+ */
40
+ /** Upper bound on render width; full-width prose is hard to read on ultra-wide terminals. */
41
+ const MAX_WIDTH = 180;
42
+ /** Fallback width when stdout is not a TTY (no `columns`). */
43
+ const FALLBACK_WIDTH = 100;
44
+ /** Floor on the content width at deep nesting, so wrapping never degenerates. */
45
+ const MIN_CONTENT_WIDTH = 12;
46
+ /** Columns a blockquote reserves for its `│ ` bar. */
47
+ const BLOCKQUOTE_RESERVE = 2;
48
+ /** cli-table3 reserves one space of padding on each side of a cell; colWidth = content + this. */
49
+ const CELL_PADDING = 2;
50
+ /** Smallest table `colWidth` we will shrink a column to — one content char plus padding. */
51
+ const MIN_COL_WIDTH = CELL_PADDING + 1;
52
+ /** Inline and block styles. Kept here so the look is tweakable in one place. */
53
+ const STYLE = {
54
+ // Bold gets a dark-gray background (palette index 236 ≈ #303030): plain
55
+ // bold is invisible in monospace fonts without a bold variant, so the
56
+ // block reads as a highlighter on any 256-colour terminal.
57
+ strong: chalk.bgAnsi256(236).bold,
58
+ em: chalk.italic,
59
+ del: chalk.dim.strikethrough,
60
+ codespan: chalk.yellow,
61
+ code: chalk.yellow,
62
+ link: chalk.blue.underline,
63
+ heading: chalk.green.bold,
64
+ firstHeading: chalk.magenta.underline.bold,
65
+ blockquoteBar: chalk.dim("│ "),
66
+ hr: chalk.dim,
67
+ html: chalk.gray,
68
+ };
69
+ /**
70
+ * Unescape the HTML entities marked emits in text and code spans, so the
71
+ * terminal shows the real characters (`&amp;` → `&`, `&lt;` → `<`, …).
72
+ *
73
+ * @param text - possibly entity-escaped string
74
+ * @returns the string with the common entities resolved
75
+ */
76
+ function unescapeEntities(text) {
77
+ return text
78
+ .replace(/&amp;/g, "&")
79
+ .replace(/&lt;/g, "<")
80
+ .replace(/&gt;/g, ">")
81
+ .replace(/&quot;/g, '"')
82
+ .replace(/&#39;/g, "'");
83
+ }
84
+ /**
85
+ * Remove OSC 8 terminal-hyperlink escape sequences, keeping the visible
86
+ * link text. The `link` renderer emits these when the terminal supports
87
+ * hyperlinks, but cli-table3's cell wrapping only understands SGR colour
88
+ * codes — a link inside a wrapped table cell would have its width
89
+ * miscounted and its bytes split mid-sequence — so table cells render
90
+ * links as plain (non-clickable) text. The sequence is
91
+ * `ESC ] 8 ; params ; URI ST text ESC ] 8 ; ; ST`, ST = BEL or `ESC \`.
92
+ *
93
+ * @param text - possibly hyperlink-bearing styled string
94
+ * @returns the string with OSC 8 wrappers removed
95
+ */
96
+ function stripHyperlinks(text) {
97
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: OSC escapes are control chars by definition.
98
+ return text.replace(/\x1b\]8;[^;]*;[^\x1b\x07]*(?:\x07|\x1b\\)/g, "");
99
+ }
100
+ /**
101
+ * Wrap an OSC 8 terminal hyperlink around visible (already-styled) text.
102
+ *
103
+ * @param text - the visible, clickable text
104
+ * @param url - the link target
105
+ * @returns the text wrapped in an OSC 8 hyperlink sequence
106
+ */
107
+ function hyperlink(text, url) {
108
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
109
+ }
110
+ /**
111
+ * Visible length of a cell's raw source text. Slightly over-estimates
112
+ * (markdown markers count), which only hands a column a touch more room
113
+ * — cli-table3 does the exact ANSI-aware wrapping itself.
114
+ *
115
+ * @param text - raw markdown cell text
116
+ * @returns character count
117
+ */
118
+ function visibleLength(text) {
119
+ return text.length;
120
+ }
121
+ /**
122
+ * Decide per-column `colWidths` for a markdown table so it fits the
123
+ * available width. Returns `undefined` when the table's natural size
124
+ * already fits — letting cli-table3 size columns to content with no
125
+ * wrapping. On overflow, distributes the available width across columns
126
+ * proportionally to their natural size, with a per-column floor.
127
+ *
128
+ * @param naturalContent - natural content width of each column (no padding)
129
+ * @param width - width to fit within
130
+ * @returns colWidths array including padding, or `undefined` for natural sizing
131
+ */
132
+ function budgetColumns(naturalContent, width) {
133
+ const columnCount = naturalContent.length;
134
+ if (columnCount === 0) {
135
+ return undefined;
136
+ }
137
+ const borders = columnCount + 1;
138
+ const naturalColWidths = naturalContent.map((content) => content + CELL_PADDING);
139
+ const naturalTotal = naturalColWidths.reduce((sum, w) => sum + w, 0) + borders;
140
+ if (naturalTotal <= width) {
141
+ return undefined;
142
+ }
143
+ const available = Math.max(width, MIN_COL_WIDTH * columnCount + borders) - borders;
144
+ const naturalSum = naturalColWidths.reduce((sum, w) => sum + w, 0);
145
+ const widths = naturalColWidths.map((w) => Math.max(MIN_COL_WIDTH, Math.round((available * w) / naturalSum)));
146
+ // Rounding and the floor can push the total back over `available`;
147
+ // trim from the widest column above the floor until it fits.
148
+ let total = widths.reduce((sum, w) => sum + w, 0);
149
+ while (total > available) {
150
+ let widestIndex = -1;
151
+ for (let i = 0; i < widths.length; i += 1) {
152
+ if (widths[i] > MIN_COL_WIDTH &&
153
+ (widestIndex === -1 || widths[i] > widths[widestIndex])) {
154
+ widestIndex = i;
155
+ }
156
+ }
157
+ if (widestIndex === -1) {
158
+ break;
159
+ }
160
+ widths[widestIndex] -= 1;
161
+ total -= 1;
162
+ }
163
+ return widths;
164
+ }
165
+ /**
166
+ * Build the marked renderer. All wrapping and indentation is driven by a
167
+ * mutable `avail` (remaining content width) captured in this closure:
168
+ * container renderers shrink it before parsing their children and
169
+ * restore it after (marked parses depth-first and synchronously, so this
170
+ * is safe), and block renderers wrap to it. The visible left margin is
171
+ * applied per line by the container as the recursion unwinds, so the
172
+ * total margin equals the columns reserved.
173
+ *
174
+ * @param width - the effective terminal width for this render
175
+ * @returns a marked `RendererObject` plus the `avail` accessor it closes over
176
+ */
177
+ function buildRenderer(width) {
178
+ let avail = width;
179
+ /** Wrap styled text to a width, emitting self-contained lines. */
180
+ const wrap = (styled, w, trim = true) => wrapAnsi(styled, Math.max(MIN_CONTENT_WIDTH, w), { hard: true, trim });
181
+ /** Run `render` with `avail` reduced by `reserve`, then restore it. */
182
+ const withReserved = (reserve, render) => {
183
+ const saved = avail;
184
+ avail = Math.max(MIN_CONTENT_WIDTH, avail - reserve);
185
+ try {
186
+ return render();
187
+ }
188
+ finally {
189
+ avail = saved;
190
+ }
191
+ };
192
+ /** Prefix the first line with `first`, the rest with `rest` (empty lines stay empty). */
193
+ const prefixLines = (text, first, rest) => text
194
+ .split("\n")
195
+ .map((line, index) => (line === "" ? "" : (index === 0 ? first : rest) + line))
196
+ .join("\n");
197
+ const parseCell = function (cell) {
198
+ return stripHyperlinks(this.parser.parseInline(cell.tokens));
199
+ };
200
+ /**
201
+ * Render one list item: render its content at the reduced width, then
202
+ * wrap and prefix the first line with the bullet/number and
203
+ * continuation lines with matching spaces (hanging indent). Nested
204
+ * lists already fit the reduced width, so re-wrapping leaves them
205
+ * untouched while still wrapping the item's own long leading text.
206
+ */
207
+ const renderItem = function (item, marker, markerWidth) {
208
+ const contentWidth = Math.max(MIN_CONTENT_WIDTH, avail - markerWidth);
209
+ const saved = avail;
210
+ avail = contentWidth;
211
+ let content;
212
+ try {
213
+ content = this.parser.parse(item.tokens, !!item.loose);
214
+ }
215
+ finally {
216
+ avail = saved;
217
+ }
218
+ let body = content.replace(/\n+$/u, "");
219
+ if (item.task) {
220
+ body = `[${item.checked ? "x" : " "}] ${body}`;
221
+ }
222
+ // trim:false so the leading indentation of nested list lines survives;
223
+ // strip only trailing whitespace that the no-trim wrap leaves behind.
224
+ const wrapped = wrap(body, contentWidth, false).replace(/[^\S\n]+$/gmu, "");
225
+ return `${prefixLines(wrapped, marker, " ".repeat(markerWidth))}\n`;
226
+ };
227
+ const renderer = {
228
+ space() {
229
+ return "";
230
+ },
231
+ text(token) {
232
+ // Block `text` tokens (tight list items) carry inline children; render
233
+ // them and add a trailing newline so a following nested list separates
234
+ // from the item's leading text. Wrapping is the caller's job — never
235
+ // wrap inline content here. Inline `text` tokens have no `.tokens` and
236
+ // hit the plain branch, so this newline is block-only.
237
+ if ("tokens" in token && Array.isArray(token.tokens) && token.tokens.length > 0) {
238
+ return `${this.parser.parseInline(token.tokens)}\n`;
239
+ }
240
+ return unescapeEntities(token.text);
241
+ },
242
+ paragraph(token) {
243
+ // trim:false so author-supplied leading indentation on hard-break
244
+ // continuation lines survives (e.g. the report's indented `Thinking:`
245
+ // / `tool_call:` lines under a bold step header); strip only the
246
+ // trailing whitespace the no-trim wrap leaves behind, mirroring
247
+ // renderItem.
248
+ const wrapped = wrap(this.parser.parseInline(token.tokens), avail, false).replace(/[^\S\n]+$/gmu, "");
249
+ return `${wrapped}\n\n`;
250
+ },
251
+ heading(token) {
252
+ const prefix = `${"#".repeat(token.depth)} `;
253
+ const style = token.depth === 1 ? STYLE.firstHeading : STYLE.heading;
254
+ return `${wrap(style(prefix + this.parser.parseInline(token.tokens)), avail)}\n\n`;
255
+ },
256
+ blockquote(token) {
257
+ const inner = withReserved(BLOCKQUOTE_RESERVE, () => this.parser.parse(token.tokens)).replace(/\n+$/u, "");
258
+ const bar = STYLE.blockquoteBar;
259
+ const body = inner
260
+ .split("\n")
261
+ .map((line) => bar + line)
262
+ .join("\n");
263
+ return `${body}\n\n`;
264
+ },
265
+ list(token) {
266
+ const ordered = token.ordered;
267
+ const start = typeof token.start === "number" && token.start ? token.start : 1;
268
+ const markerWidth = ordered ? String(start + token.items.length - 1).length + 2 : 2;
269
+ const parts = token.items.map((item, index) => {
270
+ const marker = ordered ? `${start + index}. `.padEnd(markerWidth) : "• ";
271
+ return renderItem.call(this, item, marker, markerWidth);
272
+ });
273
+ return `${parts.join("")}\n`;
274
+ },
275
+ listitem() {
276
+ // Items are rendered by `list` via `renderItem`; never reached.
277
+ return "";
278
+ },
279
+ checkbox(_token) {
280
+ // Task state is handled inline in `renderItem`; never reached.
281
+ return "";
282
+ },
283
+ code(token) {
284
+ const language = token.lang?.trim() || undefined;
285
+ if (chalk.level === 0) {
286
+ return `${token.text.replace(/\n+$/u, "")}\n\n`;
287
+ }
288
+ let body;
289
+ try {
290
+ body = highlight(token.text, { language, ignoreIllegals: true });
291
+ }
292
+ catch {
293
+ body = STYLE.code(token.text);
294
+ }
295
+ return `${body.replace(/\n+$/u, "")}\n\n`;
296
+ },
297
+ hr() {
298
+ return `${STYLE.hr("─".repeat(Math.max(MIN_CONTENT_WIDTH, avail)))}\n\n`;
299
+ },
300
+ br() {
301
+ return "\n";
302
+ },
303
+ html(token) {
304
+ return STYLE.html(token.text);
305
+ },
306
+ strong(token) {
307
+ return STYLE.strong(this.parser.parseInline(token.tokens));
308
+ },
309
+ em(token) {
310
+ return STYLE.em(this.parser.parseInline(token.tokens));
311
+ },
312
+ del(token) {
313
+ return STYLE.del(this.parser.parseInline(token.tokens));
314
+ },
315
+ codespan(token) {
316
+ return STYLE.codespan(unescapeEntities(token.text));
317
+ },
318
+ link(token) {
319
+ const text = this.parser.parseInline(token.tokens);
320
+ const href = token.href;
321
+ if (terminalSupportsHyperlinks) {
322
+ return hyperlink(STYLE.link(text || href), href);
323
+ }
324
+ if (text && text !== href) {
325
+ return `${STYLE.link(text)} (${chalk.dim(href)})`;
326
+ }
327
+ return STYLE.link(href);
328
+ },
329
+ image(token) {
330
+ return `![${token.text}](${token.href})`;
331
+ },
332
+ table(token) {
333
+ const cell = parseCell.bind(this);
334
+ const naturalContent = token.header.map((header, columnIndex) => Math.max(visibleLength(header.text), ...token.rows.map((row) => visibleLength(row[columnIndex]?.text ?? ""))));
335
+ const colWidths = budgetColumns(naturalContent, avail);
336
+ const table = new Table({
337
+ head: token.header.map(cell),
338
+ wordWrap: true,
339
+ wrapOnWordBoundary: false,
340
+ // Passing `colWidths: undefined` makes cli-table3 throw, so the
341
+ // key is omitted entirely when the table fits naturally.
342
+ ...(colWidths ? { colWidths } : {}),
343
+ });
344
+ for (const row of token.rows) {
345
+ table.push(row.map(cell));
346
+ }
347
+ return `${table.toString()}\n\n`;
348
+ },
349
+ tablerow() {
350
+ return "";
351
+ },
352
+ tablecell() {
353
+ return "";
354
+ },
355
+ };
356
+ return renderer;
357
+ }
358
+ /**
359
+ * Render markdown to ANSI-styled terminal output at the current terminal
360
+ * width. Caller is expected to have decided (e.g. via a TTY check) that
361
+ * ANSI styling is appropriate; this function does no TTY detection of its
362
+ * own and always emits styled output.
363
+ *
364
+ * Trailing newlines are preserved — call sites that print a separator
365
+ * afterwards may want to `.replace(/\n+$/, "")` to avoid compounding
366
+ * blank lines.
367
+ *
368
+ * @param input - markdown source
369
+ * @returns ANSI-styled terminal output
370
+ */
371
+ export function renderMarkdown(input) {
372
+ const width = Math.min(process.stdout.columns ?? FALLBACK_WIDTH, MAX_WIDTH);
373
+ const marked = new Marked();
374
+ marked.use({ renderer: buildRenderer(width) });
375
+ return marked.parse(input);
376
+ }