@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.
- package/build/AgentRun.d.ts +187 -0
- package/build/AgentRun.js +1 -0
- package/build/AgentRunBus.d.ts +147 -0
- package/build/AgentRunBus.js +362 -0
- package/build/Calendar.d.ts +450 -0
- package/build/Calendar.js +37 -0
- package/build/ChatMessage.d.ts +53 -0
- package/build/ChatMessage.js +11 -0
- package/build/ChatMessageBus.d.ts +110 -0
- package/build/ChatMessageBus.js +257 -0
- package/build/Config.d.ts +83 -0
- package/build/Config.js +1 -0
- package/build/ContainerTool.d.ts +34 -0
- package/build/ContainerTool.js +1 -0
- package/build/CronExpression.d.ts +53 -0
- package/build/CronExpression.js +77 -0
- package/build/Event.d.ts +280 -0
- package/build/Event.js +14 -0
- package/build/EventBus.d.ts +198 -0
- package/build/EventBus.js +482 -0
- package/build/HandlerCatalog.d.ts +75 -0
- package/build/HandlerCatalog.js +165 -0
- package/build/InferenceEvent.d.ts +30 -0
- package/build/InferenceEvent.js +1 -0
- package/build/InferenceEventBus.d.ts +39 -0
- package/build/InferenceEventBus.js +80 -0
- package/build/Mail.d.ts +250 -0
- package/build/Mail.js +73 -0
- package/build/MailStyleTemplate.d.ts +71 -0
- package/build/MailStyleTemplate.js +47 -0
- package/build/ModelMetaData.d.ts +63 -0
- package/build/ModelMetaData.js +12 -0
- package/build/PathGlob.d.ts +20 -0
- package/build/PathGlob.js +33 -0
- package/build/Plugin.d.ts +709 -0
- package/build/Plugin.js +29 -0
- package/build/PostgresConnection.d.ts +75 -0
- package/build/PostgresConnection.js +162 -0
- package/build/ScheduledSubagent.d.ts +49 -0
- package/build/ScheduledSubagent.js +1 -0
- package/build/ScheduledSubagentBus.d.ts +94 -0
- package/build/ScheduledSubagentBus.js +187 -0
- package/build/Schema.d.ts +84 -0
- package/build/Schema.js +570 -0
- package/build/StepResult.d.ts +103 -0
- package/build/StepResult.js +1 -0
- package/build/StepResultBus.d.ts +64 -0
- package/build/StepResultBus.js +169 -0
- package/build/Timezone.d.ts +58 -0
- package/build/Timezone.js +84 -0
- package/build/ToolCall.d.ts +25 -0
- package/build/ToolCall.js +1 -0
- package/build/ToolCallBus.d.ts +65 -0
- package/build/ToolCallBus.js +110 -0
- package/build/ToolLevel.d.ts +20 -0
- package/build/ToolLevel.js +2 -0
- package/build/ToolRunner.d.ts +118 -0
- package/build/ToolRunner.js +192 -0
- package/build/ToolsExpressionUtils.d.ts +80 -0
- package/build/ToolsExpressionUtils.js +102 -0
- package/build/WorkspaceFile.d.ts +104 -0
- package/build/WorkspaceFile.js +7 -0
- package/build/estimateTokens.d.ts +16 -0
- package/build/estimateTokens.js +18 -0
- package/build/index.d.ts +37 -0
- package/build/index.js +26 -0
- package/build/logging/Logger.d.ts +66 -0
- package/build/logging/Logger.js +90 -0
- package/build/markdownHtml.d.ts +14 -0
- package/build/markdownHtml.js +21 -0
- package/build/markdownTerminal.d.ts +14 -0
- package/build/markdownTerminal.js +376 -0
- package/build/testing/MockAgentRunBus.d.ts +52 -0
- package/build/testing/MockAgentRunBus.js +175 -0
- package/build/testing/MockBusStore.d.ts +22 -0
- package/build/testing/MockBusStore.js +22 -0
- package/build/testing/MockEventBus.d.ts +20 -0
- package/build/testing/MockEventBus.js +57 -0
- package/build/testing/MockLogger.d.ts +52 -0
- package/build/testing/MockLogger.js +65 -0
- package/build/testing/index.d.ts +4 -0
- package/build/testing/index.js +4 -0
- package/package.json +63 -0
package/build/index.d.ts
ADDED
|
@@ -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 (`&` → `&`, `<` → `<`, …).
|
|
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(/&/g, "&")
|
|
79
|
+
.replace(/</g, "<")
|
|
80
|
+
.replace(/>/g, ">")
|
|
81
|
+
.replace(/"/g, '"')
|
|
82
|
+
.replace(/'/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 ``;
|
|
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
|
+
}
|