tinker-agent 1.0.65 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +90 -9
  5. package/bin/tinker.js +39 -0
  6. package/node_modules/ansi-regex/index.d.ts +33 -0
  7. package/node_modules/ansi-regex/index.js +14 -0
  8. package/node_modules/ansi-regex/license +9 -0
  9. package/node_modules/ansi-regex/package.json +61 -0
  10. package/node_modules/ansi-regex/readme.md +66 -0
  11. package/node_modules/ansi-styles/index.d.ts +236 -0
  12. package/node_modules/ansi-styles/index.js +223 -0
  13. package/node_modules/ansi-styles/license +9 -0
  14. package/node_modules/ansi-styles/package.json +54 -0
  15. package/node_modules/ansi-styles/readme.md +173 -0
  16. package/node_modules/chalk/license +9 -0
  17. package/node_modules/chalk/package.json +83 -0
  18. package/node_modules/chalk/readme.md +297 -0
  19. package/node_modules/chalk/source/index.d.ts +325 -0
  20. package/node_modules/chalk/source/index.js +225 -0
  21. package/node_modules/chalk/source/utilities.js +33 -0
  22. package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  23. package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  24. package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  25. package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  26. package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  27. package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  28. package/node_modules/character-entities/index.d.ts +6 -0
  29. package/node_modules/character-entities/index.js +2132 -0
  30. package/node_modules/character-entities/license +22 -0
  31. package/node_modules/character-entities/package.json +78 -0
  32. package/node_modules/character-entities/readme.md +152 -0
  33. package/node_modules/decode-named-character-reference/index.d.ts +13 -0
  34. package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
  35. package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
  36. package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
  37. package/node_modules/decode-named-character-reference/index.dom.js +32 -0
  38. package/node_modules/decode-named-character-reference/index.js +19 -0
  39. package/node_modules/decode-named-character-reference/license +22 -0
  40. package/node_modules/decode-named-character-reference/package.json +90 -0
  41. package/node_modules/decode-named-character-reference/readme.md +136 -0
  42. package/node_modules/get-east-asian-width/index.d.ts +60 -0
  43. package/node_modules/get-east-asian-width/index.js +30 -0
  44. package/node_modules/get-east-asian-width/license +9 -0
  45. package/node_modules/get-east-asian-width/lookup-data.js +21 -0
  46. package/node_modules/get-east-asian-width/lookup.js +138 -0
  47. package/node_modules/get-east-asian-width/package.json +71 -0
  48. package/node_modules/get-east-asian-width/readme.md +65 -0
  49. package/node_modules/get-east-asian-width/utilities.js +24 -0
  50. package/node_modules/has-flag/index.d.ts +39 -0
  51. package/node_modules/has-flag/index.js +8 -0
  52. package/node_modules/has-flag/license +9 -0
  53. package/node_modules/has-flag/package.json +49 -0
  54. package/node_modules/has-flag/readme.md +74 -0
  55. package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
  56. package/node_modules/is-fullwidth-code-point/index.js +12 -0
  57. package/node_modules/is-fullwidth-code-point/license +9 -0
  58. package/node_modules/is-fullwidth-code-point/package.json +53 -0
  59. package/node_modules/is-fullwidth-code-point/readme.md +31 -0
  60. package/node_modules/markdansi/LICENSE +21 -0
  61. package/node_modules/markdansi/README.md +167 -0
  62. package/node_modules/markdansi/dist/ast.d.ts +99 -0
  63. package/node_modules/markdansi/dist/ast.js +1 -0
  64. package/node_modules/markdansi/dist/cli.d.ts +17 -0
  65. package/node_modules/markdansi/dist/cli.js +208 -0
  66. package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
  67. package/node_modules/markdansi/dist/hyperlink.js +26 -0
  68. package/node_modules/markdansi/dist/index.d.ts +10 -0
  69. package/node_modules/markdansi/dist/index.js +16 -0
  70. package/node_modules/markdansi/dist/parser.d.ts +2 -0
  71. package/node_modules/markdansi/dist/parser.js +205 -0
  72. package/node_modules/markdansi/dist/render.d.ts +9 -0
  73. package/node_modules/markdansi/dist/render.js +680 -0
  74. package/node_modules/markdansi/dist/stream.d.ts +39 -0
  75. package/node_modules/markdansi/dist/stream.js +192 -0
  76. package/node_modules/markdansi/dist/theme.d.ts +18 -0
  77. package/node_modules/markdansi/dist/theme.js +105 -0
  78. package/node_modules/markdansi/dist/types.d.ts +58 -0
  79. package/node_modules/markdansi/dist/types.js +1 -0
  80. package/node_modules/markdansi/dist/wrap.d.ts +10 -0
  81. package/node_modules/markdansi/dist/wrap.js +73 -0
  82. package/node_modules/markdansi/docs/spec.md +110 -0
  83. package/node_modules/markdansi/package.json +78 -0
  84. package/node_modules/markdansi/tsconfig.json +18 -0
  85. package/node_modules/marked/LICENSE +44 -0
  86. package/node_modules/marked/README.md +115 -0
  87. package/node_modules/marked/bin/main.js +284 -0
  88. package/node_modules/marked/bin/marked.js +16 -0
  89. package/node_modules/marked/lib/marked.d.ts +760 -0
  90. package/node_modules/marked/lib/marked.esm.js +77 -0
  91. package/node_modules/marked/lib/marked.esm.js.map +7 -0
  92. package/node_modules/marked/lib/marked.umd.js +79 -0
  93. package/node_modules/marked/lib/marked.umd.js.map +7 -0
  94. package/node_modules/marked/man/marked.1 +113 -0
  95. package/node_modules/marked/man/marked.1.md +93 -0
  96. package/node_modules/marked/package.json +103 -0
  97. package/node_modules/slice-ansi/index.d.ts +19 -0
  98. package/node_modules/slice-ansi/index.js +317 -0
  99. package/node_modules/slice-ansi/license +10 -0
  100. package/node_modules/slice-ansi/package.json +59 -0
  101. package/node_modules/slice-ansi/readme.md +55 -0
  102. package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
  103. package/node_modules/string-width/index.d.ts +39 -0
  104. package/node_modules/string-width/index.js +203 -0
  105. package/node_modules/string-width/license +9 -0
  106. package/node_modules/string-width/package.json +65 -0
  107. package/node_modules/string-width/readme.md +66 -0
  108. package/node_modules/strip-ansi/index.d.ts +15 -0
  109. package/node_modules/strip-ansi/index.js +19 -0
  110. package/node_modules/strip-ansi/license +9 -0
  111. package/node_modules/strip-ansi/package.json +59 -0
  112. package/node_modules/strip-ansi/readme.md +37 -0
  113. package/node_modules/supports-color/browser.d.ts +1 -0
  114. package/node_modules/supports-color/browser.js +35 -0
  115. package/node_modules/supports-color/index.d.ts +55 -0
  116. package/node_modules/supports-color/index.js +202 -0
  117. package/node_modules/supports-color/license +9 -0
  118. package/node_modules/supports-color/package.json +64 -0
  119. package/node_modules/supports-color/readme.md +75 -0
  120. package/node_modules/supports-hyperlinks/browser.js +10 -0
  121. package/node_modules/supports-hyperlinks/index.d.ts +25 -0
  122. package/node_modules/supports-hyperlinks/index.js +163 -0
  123. package/node_modules/supports-hyperlinks/license +10 -0
  124. package/node_modules/supports-hyperlinks/package.json +47 -0
  125. package/node_modules/supports-hyperlinks/readme.md +45 -0
  126. package/package.json +41 -10
  127. package/patches/markdansi@0.3.2.patch +22 -2
  128. package/src/agent/context-builder.ts +24 -10
  129. package/src/agent/context-meter.ts +89 -5
  130. package/src/agent/loop.ts +107 -10
  131. package/src/agent/runtime-session.ts +1437 -82
  132. package/src/agent/session-ledger.ts +169 -29
  133. package/src/agent/types.ts +12 -1
  134. package/src/agent/user-prompt-projection.ts +62 -0
  135. package/src/cli/config.ts +57 -14
  136. package/src/cli/model-profiles.ts +124 -0
  137. package/src/cli/run-runner.ts +4 -1
  138. package/src/cli/tui-runner.tsx +68 -30
  139. package/src/context/compiled-context-hash.ts +1 -1
  140. package/src/context/compiled-context-validator.ts +92 -15
  141. package/src/context/context-automation-policy.ts +121 -0
  142. package/src/context/context-manager.ts +390 -11
  143. package/src/context/context-policy.ts +8 -0
  144. package/src/context/context-protocol-validator.ts +11 -1
  145. package/src/context/context-revision-compiler.ts +190 -27
  146. package/src/context/context-revision.ts +105 -19
  147. package/src/context/context-surface.ts +329 -0
  148. package/src/context/prefix-retirement-planner.ts +570 -0
  149. package/src/context/protocol-frame.ts +30 -3
  150. package/src/context/recall-retirement-contract.ts +27 -0
  151. package/src/context/swap-planner.ts +34 -10
  152. package/src/events/observation-text-log.ts +3 -3
  153. package/src/events/stdout-event-printer.ts +39 -3
  154. package/src/events/types.ts +172 -35
  155. package/src/ids/runtime-id.ts +6 -0
  156. package/src/ids/uuid-v7.ts +7 -0
  157. package/src/image/image-asset-store.ts +319 -0
  158. package/src/image/image-input-policy.ts +14 -0
  159. package/src/image/image-probe.ts +233 -0
  160. package/src/image/image-types.ts +262 -0
  161. package/src/instructions/project-instructions.ts +4 -1
  162. package/src/mcp/mcp-manager.ts +83 -19
  163. package/src/model/fake-model-client.ts +9 -1
  164. package/src/model/input-token-estimator.ts +25 -0
  165. package/src/model/model-client.ts +60 -0
  166. package/src/model/model-request-preflight.ts +1 -0
  167. package/src/model/moonshot-input-token-estimator.ts +107 -0
  168. package/src/model/openai-chat-mapping.ts +77 -1
  169. package/src/model/openai-chat-model-client.ts +389 -59
  170. package/src/model/openai-chat-stream.ts +272 -0
  171. package/src/model/prompt-prefix-hash.ts +2 -4
  172. package/src/model/token-estimator.ts +4 -0
  173. package/src/observation/observation-builder.ts +29 -0
  174. package/src/session/resume-projection.ts +94 -34
  175. package/src/session/session-catalog.ts +4 -4
  176. package/src/session/session-errors.ts +1 -1
  177. package/src/session/session-history-reader.ts +151 -4
  178. package/src/session/session-last-response-reader.ts +189 -0
  179. package/src/session/session-schema.ts +349 -35
  180. package/src/session/session-store.ts +3266 -255
  181. package/src/session/sqlite-session-ledger.ts +9 -4
  182. package/src/skills/skill-catalog.ts +182 -0
  183. package/src/skills/skill-context.ts +364 -0
  184. package/src/skills/skill-loader.ts +779 -0
  185. package/src/skills/skill-tool.ts +297 -0
  186. package/src/tools/edit.ts +51 -64
  187. package/src/tools/ensure-parent-directory.ts +6 -0
  188. package/src/tools/read.ts +2 -2
  189. package/src/tools/recall.ts +43 -40
  190. package/src/tools/registry.ts +24 -3
  191. package/src/tools/ripgrep.ts +3 -2
  192. package/src/tools/types.ts +43 -4
  193. package/src/tools/write.ts +20 -31
  194. package/src/tui/app.tsx +333 -96
  195. package/src/tui/clipboard.ts +5 -0
  196. package/src/tui/components/mcp-panel.tsx +28 -0
  197. package/src/tui/components/prompt-input.tsx +790 -257
  198. package/src/tui/components/skills-panel.tsx +31 -0
  199. package/src/tui/components/timeline.tsx +30 -1
  200. package/src/tui/event-store.ts +85 -2
  201. package/src/tui/file-mention.ts +234 -0
  202. package/src/tui/line-editor.ts +1 -0
  203. package/src/tui/project-slash-commands.ts +229 -0
  204. package/src/tui/prompt-draft.ts +510 -0
  205. package/src/tui/prompt-history.ts +294 -22
  206. package/src/tui/slash-commands.ts +46 -2
  207. package/src/tui/tui-session-controller.ts +110 -48
  208. package/src/tui/workspace-file-search.ts +94 -0
@@ -0,0 +1,680 @@
1
+ import sliceAnsi from "slice-ansi";
2
+ import stringWidth from "string-width";
3
+ import stripAnsi from "strip-ansi";
4
+ import { hyperlinkSupported, osc8 } from "./hyperlink.js";
5
+ import { parse } from "./parser.js";
6
+ import { createStyler, themes } from "./theme.js";
7
+ import { visibleWidth, wrapText, wrapWithPrefix } from "./wrap.js";
8
+ function dedent(markdown) {
9
+ const lines = markdown.split("\n");
10
+ const indents = lines
11
+ .filter((l) => l.trim() !== "")
12
+ .map((l) => l.match(/^[ \t]*/)?.[0].length ?? 0);
13
+ if (indents.length === 0)
14
+ return markdown;
15
+ const minIndent = Math.min(...indents);
16
+ if (minIndent === 0)
17
+ return markdown;
18
+ return lines.map((l) => l.slice(Math.min(minIndent, l.length))).join("\n");
19
+ }
20
+ function resolveOptions(userOptions = {}) {
21
+ const wrap = userOptions.wrap !== undefined ? userOptions.wrap : true;
22
+ const baseWidth = userOptions.width ?? (wrap ? process.stdout.columns || 80 : undefined);
23
+ const color = userOptions.color !== undefined ? userOptions.color : process.stdout.isTTY;
24
+ // OSC hyperlinks require color support; if color is off, force hyperlinks off too
25
+ const hyperlinks = userOptions.hyperlinks !== undefined ? userOptions.hyperlinks : color && hyperlinkSupported();
26
+ const effectiveHyperlinks = color ? hyperlinks : false;
27
+ const baseTheme = themes.default ?? {};
28
+ const userTheme = userOptions.theme && typeof userOptions.theme === "object"
29
+ ? userOptions.theme
30
+ : themes[userOptions.theme || "default"] || baseTheme;
31
+ const mergedTheme = {
32
+ ...baseTheme,
33
+ ...userTheme,
34
+ inlineCode: userTheme?.inlineCode || userTheme?.code || baseTheme.inlineCode || baseTheme.code || {},
35
+ blockCode: userTheme?.blockCode || userTheme?.code || baseTheme.blockCode || baseTheme.code || {},
36
+ };
37
+ const highlighter = userOptions.highlighter;
38
+ const listIndent = userOptions.listIndent ?? 2;
39
+ const quotePrefix = userOptions.quotePrefix ?? "│ ";
40
+ const tableBorder = userOptions.tableBorder || "unicode";
41
+ const tablePadding = userOptions.tablePadding ?? 1;
42
+ const tableDense = userOptions.tableDense ?? false;
43
+ const tableTruncate = userOptions.tableTruncate ?? true;
44
+ const tableEllipsis = userOptions.tableEllipsis ?? "…";
45
+ const codeBox = userOptions.codeBox ?? true;
46
+ const codeGutter = userOptions.codeGutter ?? false;
47
+ const codeWrap = userOptions.codeWrap ?? true;
48
+ const resolved = {
49
+ wrap,
50
+ color,
51
+ hyperlinks: effectiveHyperlinks,
52
+ theme: mergedTheme,
53
+ highlighter,
54
+ listIndent,
55
+ quotePrefix,
56
+ tableBorder,
57
+ tablePadding,
58
+ tableDense,
59
+ tableTruncate,
60
+ tableEllipsis,
61
+ codeBox,
62
+ codeGutter,
63
+ codeWrap,
64
+ };
65
+ if (baseWidth !== undefined)
66
+ resolved.width = baseWidth;
67
+ return resolved;
68
+ }
69
+ /**
70
+ * Normalize parsed nodes to avoid misclassifying reference-style metadata as code.
71
+ * Some sources emit footnote-like link definitions where the title is placed
72
+ * on the following indented lines (often copied with tabs). When GFM parsing
73
+ * misses the closing quote, those continuations become indented code blocks.
74
+ * To keep output readable, merge such continuations back into the definition
75
+ * paragraph and render as regular text.
76
+ */
77
+ function extractText(node) {
78
+ if (typeof node.value === "string")
79
+ return node.value;
80
+ if (Array.isArray(node.children)) {
81
+ return node.children.map((child) => extractText(child)).join("");
82
+ }
83
+ return "";
84
+ }
85
+ function getParagraphText(node) {
86
+ return extractText(node);
87
+ }
88
+ function normalizeNodes(tree) {
89
+ const normalized = [];
90
+ for (let i = 0; i < tree.children.length; i += 1) {
91
+ const node = tree.children[i];
92
+ if (node?.type === "paragraph" && node.children.length >= 1) {
93
+ const text = getParagraphText(node);
94
+ const defMatch = text.match(/^\[(\d+|\w+)]:\s+\S.*"\s*$/);
95
+ const next = tree.children[i + 1];
96
+ if (defMatch && next?.type === "code" && !next.lang) {
97
+ const continuation = next.value
98
+ .replace(/^[ \t>]+/gm, " ")
99
+ .replace(/\s+/g, " ")
100
+ .trim();
101
+ const merged = `${text.replace(/\s+"$/, '"')} ${continuation ? continuation.replace(/^"+|"+$/g, "").trim() : ""}`.trim();
102
+ normalized.push({
103
+ type: "paragraph",
104
+ children: [{ type: "text", value: merged }],
105
+ position: node.position,
106
+ });
107
+ i += 1; // skip the consumed code block
108
+ continue;
109
+ }
110
+ }
111
+ // As a fallback, rewrite indented code blocks that look like lone definitions.
112
+ if (node?.type === "code" && !node.lang) {
113
+ const stripped = node.value.replace(/^[ \t>]+/gm, "").trim();
114
+ if (/^\[(\d+|\w+)]:\s+\S+/.test(stripped)) {
115
+ normalized.push({
116
+ type: "paragraph",
117
+ children: [{ type: "text", value: stripped }],
118
+ position: node.position,
119
+ });
120
+ continue;
121
+ }
122
+ }
123
+ if (node)
124
+ normalized.push(node);
125
+ }
126
+ const mergedCodes = mergeAdjacentCodeBlocks(normalized);
127
+ const taggedDiffs = mergedCodes.map((child) => tagDiffBlock(child));
128
+ return { ...tree, children: taggedDiffs };
129
+ }
130
+ function flattenCodeList(list) {
131
+ if (!list.children.length ||
132
+ !list.children.every((item) => item.children.length === 1 &&
133
+ item.children[0]?.type === "code" &&
134
+ item.children[0].value !== undefined))
135
+ return null;
136
+ const codes = list.children.map((item) => item.children[0]);
137
+ const sameLang = codes.every((c) => c.lang === codes[0]?.lang);
138
+ const lang = sameLang ? (codes[0]?.lang ?? undefined) : undefined;
139
+ return {
140
+ type: "code",
141
+ lang: lang ?? undefined,
142
+ value: codes.map((c) => c.value).join("\n"),
143
+ position: list.position,
144
+ };
145
+ }
146
+ function mergeAdjacentCodeBlocks(nodes) {
147
+ const out = [];
148
+ let pending = null;
149
+ const flush = () => {
150
+ if (pending) {
151
+ out.push(pending);
152
+ pending = null;
153
+ }
154
+ };
155
+ for (const node of nodes) {
156
+ if (node?.type === "code") {
157
+ if (pending && (pending.lang === node.lang || (!pending.lang && !node.lang))) {
158
+ const nextValue = `${pending.value}\n${node.value}`;
159
+ pending = {
160
+ type: "code",
161
+ lang: pending.lang,
162
+ meta: pending.meta,
163
+ value: nextValue,
164
+ position: pending.position,
165
+ };
166
+ }
167
+ else {
168
+ flush();
169
+ pending = node;
170
+ }
171
+ continue;
172
+ }
173
+ if (node?.type === "list") {
174
+ const flattened = flattenCodeList(node);
175
+ if (flattened) {
176
+ if (pending && (pending.lang === flattened.lang || (!pending.lang && !flattened.lang))) {
177
+ const nextValue = `${pending.value}\n${flattened.value}`;
178
+ pending = {
179
+ type: "code",
180
+ lang: pending.lang,
181
+ meta: pending.meta,
182
+ value: nextValue,
183
+ position: pending.position,
184
+ };
185
+ }
186
+ else {
187
+ flush();
188
+ pending = flattened;
189
+ }
190
+ continue;
191
+ }
192
+ }
193
+ flush();
194
+ out.push(node);
195
+ }
196
+ flush();
197
+ return out;
198
+ }
199
+ function looksLikeDiff(text) {
200
+ const lines = text.split("\n").map((l) => l.trim());
201
+ if (lines.some((l) => l.startsWith("diff --git") ||
202
+ l.startsWith("--- a/") ||
203
+ l.startsWith("+++ b/") ||
204
+ l.startsWith("@@ ")))
205
+ return true;
206
+ const nonEmpty = lines.filter((l) => l !== "");
207
+ if (nonEmpty.length < 3)
208
+ return false;
209
+ const markers = nonEmpty.filter((l) => /^[+\-@]/.test(l)).length;
210
+ return markers >= Math.max(3, Math.ceil(nonEmpty.length * 0.6));
211
+ }
212
+ function tagDiffBlock(node) {
213
+ if (node?.type === "code" && !node.lang && looksLikeDiff(node.value)) {
214
+ return { ...node, lang: "diff" };
215
+ }
216
+ return node;
217
+ }
218
+ const HR_WIDTH = 40;
219
+ const MAX_COL = 40;
220
+ const TABLE_BOX = {
221
+ unicode: {
222
+ topLeft: "┌",
223
+ topRight: "┐",
224
+ bottomLeft: "└",
225
+ bottomRight: "┘",
226
+ hSep: "─",
227
+ vSep: "│",
228
+ tSep: "┬",
229
+ mSep: "┼",
230
+ bSep: "┴",
231
+ mLeft: "├",
232
+ mRight: "┤",
233
+ },
234
+ ascii: {
235
+ topLeft: "+",
236
+ topRight: "+",
237
+ bottomLeft: "+",
238
+ bottomRight: "+",
239
+ hSep: "-",
240
+ vSep: "|",
241
+ tSep: "+",
242
+ mSep: "+",
243
+ bSep: "+",
244
+ mLeft: "+",
245
+ mRight: "+",
246
+ },
247
+ };
248
+ /**
249
+ * Render Markdown input to an ANSI string.
250
+ */
251
+ export function render(markdown, userOptions = {}) {
252
+ const options = resolveOptions(userOptions);
253
+ const style = createStyler({ color: options.color });
254
+ const tree = normalizeNodes(parse(dedent(markdown)));
255
+ const ctx = { options, style };
256
+ const body = renderChildren(tree.children, ctx, 0, true).join("");
257
+ return options.color ? body : stripAnsi(body);
258
+ }
259
+ /**
260
+ * Create a reusable renderer with fixed options.
261
+ */
262
+ export function createRenderer(options) {
263
+ return (md) => render(md, options);
264
+ }
265
+ function renderChildren(children, ctx, indentLevel = 0, isTightList = false) {
266
+ const out = [];
267
+ for (let i = 0; i < children.length; i += 1) {
268
+ const node = children[i];
269
+ if (!node)
270
+ continue;
271
+ // Heuristic: some sources emit a standalone "[lang]" line before a fenced block.
272
+ if (node.type === "paragraph" &&
273
+ node.children.length === 1 &&
274
+ node.children[0]?.type === "text") {
275
+ const langMatch = node.children[0]?.value.trim().match(/^\[([^\]]+)]$/);
276
+ const next = children[i + 1];
277
+ if (langMatch && next && next.type === "code" && !next.lang) {
278
+ next.lang = langMatch[1];
279
+ i += 1; // skip label paragraph, render the code next
280
+ out.push(renderNode(next, ctx, indentLevel, isTightList));
281
+ continue;
282
+ }
283
+ }
284
+ out.push(renderNode(node, ctx, indentLevel, isTightList));
285
+ }
286
+ return out.flat();
287
+ }
288
+ function renderNode(node, ctx, indentLevel, isTightList) {
289
+ switch (node.type) {
290
+ case "paragraph":
291
+ return renderParagraph(node, ctx, indentLevel);
292
+ case "heading":
293
+ return renderHeading(node, ctx);
294
+ case "thematicBreak":
295
+ return renderHr(ctx);
296
+ case "blockquote":
297
+ return renderBlockquote(node, ctx, indentLevel);
298
+ case "list":
299
+ return renderList(node, ctx, indentLevel);
300
+ case "listItem":
301
+ return renderListItem(node, ctx, indentLevel, isTightList);
302
+ case "code":
303
+ return renderCodeBlock(node, ctx);
304
+ case "table":
305
+ return renderTable(node, ctx);
306
+ case "definition":
307
+ return renderDefinition(node, ctx);
308
+ default:
309
+ return []; // inline handled elsewhere or intentionally skipped
310
+ }
311
+ }
312
+ function renderParagraph(node, ctx, indentLevel) {
313
+ const text = normalizeParagraphInlineText(renderInline(node.children, ctx));
314
+ const prefix = " ".repeat(ctx.options.listIndent * indentLevel);
315
+ const rawLines = text.split("\n");
316
+ const normalized = [];
317
+ const defPattern = /^\[[^\]]+]:\s+\S/;
318
+ let inDefinitions = false;
319
+ for (const line of rawLines) {
320
+ if (defPattern.test(line) && normalized.length > 0 && normalized.at(-1) !== "") {
321
+ normalized.push(""); // insert blank line before footer-style definitions
322
+ }
323
+ if (defPattern.test(line)) {
324
+ inDefinitions = true;
325
+ normalized.push(line);
326
+ continue;
327
+ }
328
+ if (inDefinitions && line.trim() === "") {
329
+ // skip extra blank lines inside the definitions block
330
+ continue;
331
+ }
332
+ inDefinitions = false;
333
+ normalized.push(line);
334
+ }
335
+ const lines = normalized.flatMap((l) => wrapWithPrefix(l, ctx.options.width ?? 80, ctx.options.wrap, prefix));
336
+ return lines.map((l) => `${l}\n`);
337
+ }
338
+ function renderHeading(node, ctx) {
339
+ const text = renderInline(node.children, ctx);
340
+ const styled = ctx.style(text, ctx.options.theme.heading);
341
+ return [`\n${styled}\n`];
342
+ }
343
+ function renderHr(ctx) {
344
+ const width = ctx.options.wrap ? Math.min(ctx.options.width ?? HR_WIDTH, HR_WIDTH) : HR_WIDTH;
345
+ const line = "—".repeat(width);
346
+ return [`${ctx.style(line, ctx.options.theme.hr)}\n`];
347
+ }
348
+ function renderBlockquote(node, ctx, indentLevel) {
349
+ // Render blockquote children as text, then wrap with the quote prefix so
350
+ // wrapping accounts for prefix width.
351
+ const inner = renderChildren(node.children, ctx, indentLevel);
352
+ const prefix = ctx.style(ctx.options.quotePrefix, ctx.options.theme.quote);
353
+ const text = inner.join("").trimEnd();
354
+ const wrapped = wrapWithPrefix(text, ctx.options.width ?? 80, ctx.options.wrap, prefix);
355
+ return wrapped.map((l) => `${l}\n`);
356
+ }
357
+ function renderList(node, ctx, indentLevel) {
358
+ const tight = node.spread === false;
359
+ const items = node.children.flatMap((item, idx) => renderListItem(item, ctx, indentLevel, tight, Boolean(node.ordered), node.start ?? 1, idx));
360
+ return items;
361
+ }
362
+ function renderListItem(node, ctx, indentLevel, tight, ordered = false, start = 1, idx = 0) {
363
+ const marker = ordered ? `${start + idx}.` : "-";
364
+ const markerStyled = ctx.style(marker, ctx.options.theme.listMarker);
365
+ const content = renderChildren(node.children, ctx, indentLevel + 1, tight)
366
+ .join("")
367
+ .trimEnd()
368
+ .split("\n");
369
+ // Drop leading blank lines so bullets prefix real content (e.g., headings in lists)
370
+ while (content.length && (content[0]?.trim() ?? "") === "") {
371
+ content.shift();
372
+ }
373
+ const isTask = typeof node.checked === "boolean";
374
+ const box = isTask && node.checked ? "[x]" : "[ ]";
375
+ const firstBullet = " ".repeat(ctx.options.listIndent * indentLevel) +
376
+ (isTask ? `${ctx.style(box, ctx.options.theme.listMarker)} ` : `${markerStyled} `);
377
+ const lines = [];
378
+ content.forEach((line, i) => {
379
+ const clean = line.replace(/^\s+/, "");
380
+ const prefix = i === 0
381
+ ? firstBullet
382
+ : `${" ".repeat(ctx.options.listIndent * indentLevel)}${" ".repeat(ctx.options.listIndent)}`;
383
+ lines.push(prefix + clean);
384
+ });
385
+ if (!tight)
386
+ lines.push("");
387
+ return lines.map((l) => `${l}\n`);
388
+ }
389
+ function renderDefinition(node, _ctx) {
390
+ const title = node.title ? ` "${node.title}"` : "";
391
+ const line = `[${node.identifier}]: ${node.url ?? ""}${title}`;
392
+ // Insert a leading blank line to visually separate footers from body.
393
+ return [`\n${line}\n`];
394
+ }
395
+ function renderCodeBlock(node, ctx) {
396
+ const theme = ctx.options.theme.blockCode || ctx.options.theme.inlineCode;
397
+ const lines = (node.value ?? "").split("\n");
398
+ const isDiff = node.lang === "diff";
399
+ const gutterWidth = ctx.options.codeGutter ? String(lines.length).length + 2 : 0;
400
+ const shouldWrap = isDiff ? false : ctx.options.codeWrap;
401
+ const useBox = ctx.options.codeBox && lines.length > 1;
402
+ const boxPadding = useBox ? 4 : 0;
403
+ const wrapLimit = shouldWrap && ctx.options.wrap && ctx.options.width
404
+ ? Math.max(1, ctx.options.width - boxPadding - gutterWidth)
405
+ : undefined; // undefined => no hard wrap limit
406
+ const contentLines = lines.flatMap((line, idx) => {
407
+ const segments = wrapLimit !== undefined ? wrapCodeLine(line, wrapLimit) : [line];
408
+ return segments.map((segment, segIdx) => {
409
+ const highlighted = ctx.options.highlighter?.(segment, node.lang ?? undefined) ?? ctx.style(segment, theme);
410
+ if (!ctx.options.codeGutter)
411
+ return highlighted;
412
+ const num = segIdx === 0 ? String(idx + 1).padStart(gutterWidth - 2, " ") : " ".repeat(gutterWidth - 1);
413
+ return `${ctx.style(num, { dim: true })} ${highlighted}`;
414
+ });
415
+ });
416
+ if (!useBox) {
417
+ return [`${contentLines.join("\n")}\n\n`];
418
+ }
419
+ // Boxed block
420
+ const maxLine = Math.max(...contentLines.map((l) => visibleWidth(l)), 0);
421
+ const minInner = node.lang ? node.lang.length + 2 : 0;
422
+ const wrapTarget = ctx.options.codeWrap && ctx.options.width
423
+ ? Math.min(maxLine, Math.max(1, ctx.options.width - 4))
424
+ : maxLine;
425
+ const labelRaw = node.lang ? `[${node.lang}]` : "";
426
+ const labelStyled = labelRaw ? ctx.style(labelRaw, { dim: true }) : "";
427
+ const innerWidth = Math.max(ctx.options.codeWrap ? wrapTarget : maxLine, minInner, labelRaw.length);
428
+ const topPadding = Math.max(0, innerWidth - labelRaw.length + 1);
429
+ const topRaw = labelRaw.length > 0
430
+ ? `┌ ${labelStyled}${"─".repeat(topPadding)}┐`
431
+ : `┌ ${"─".repeat(innerWidth)} ┐`;
432
+ const bottomRaw = `└${"─".repeat(innerWidth + 2)}┘`;
433
+ const top = ctx.style(topRaw, { dim: true });
434
+ const bottom = ctx.style(bottomRaw, { dim: true });
435
+ const boxLines = contentLines.map((ln) => {
436
+ const pad = Math.max(0, innerWidth - visibleWidth(ln));
437
+ const left = ctx.style("│ ", { dim: true });
438
+ const right = ctx.style(" │", { dim: true });
439
+ return `${left}${ln}${" ".repeat(pad)}${right}`;
440
+ });
441
+ return [`${top}\n${boxLines.join("\n")}\n${bottom}\n\n`];
442
+ }
443
+ function renderInline(children, ctx) {
444
+ let out = "";
445
+ for (const node of children) {
446
+ switch (node.type) {
447
+ case "text":
448
+ out += node.value;
449
+ break;
450
+ case "emphasis":
451
+ out += ctx.style(renderInline(node.children, ctx), ctx.options.theme.emph);
452
+ break;
453
+ case "strong":
454
+ out += ctx.style(renderInline(node.children, ctx), ctx.options.theme.strong);
455
+ break;
456
+ case "delete":
457
+ out += ctx.style(renderInline(node.children, ctx), { strike: true });
458
+ break;
459
+ case "inlineCode": {
460
+ const codeTheme = ctx.options.theme.inlineCode || ctx.options.theme.blockCode;
461
+ const content = ctx.style(node.value, codeTheme);
462
+ out += content;
463
+ break;
464
+ }
465
+ case "link":
466
+ out += renderLink(node, ctx);
467
+ break;
468
+ case "break":
469
+ out += HARD_BREAK;
470
+ break;
471
+ case "html":
472
+ out += node.value;
473
+ break;
474
+ }
475
+ }
476
+ return out;
477
+ }
478
+ const HARD_BREAK = "\u000B";
479
+ function normalizeParagraphInlineText(text) {
480
+ if (!text.includes("\n") && !text.includes(HARD_BREAK))
481
+ return text;
482
+ const segments = [];
483
+ let current = "";
484
+ for (let i = 0; i < text.length; i += 1) {
485
+ const ch = text[i];
486
+ if (ch === "\n" || ch === HARD_BREAK) {
487
+ segments.push({
488
+ text: current,
489
+ breakAfter: ch === HARD_BREAK ? "hard" : "soft",
490
+ });
491
+ current = "";
492
+ continue;
493
+ }
494
+ current += ch;
495
+ }
496
+ segments.push({ text: current });
497
+ const defPattern = /^\[[^\]]+]:\s+\S/;
498
+ let out = segments[0]?.text ?? "";
499
+ for (let i = 0; i < segments.length - 1; i += 1) {
500
+ const kind = segments[i]?.breakAfter ?? "soft";
501
+ const left = segments[i]?.text ?? "";
502
+ const right = segments[i + 1]?.text ?? "";
503
+ if (kind === "hard") {
504
+ out += "\n";
505
+ out += right;
506
+ continue;
507
+ }
508
+ const leftTrim = left.trimStart();
509
+ const rightTrim = right.trimStart();
510
+ const keepNewline = left === "" || right === "" || defPattern.test(leftTrim) || defPattern.test(rightTrim);
511
+ out += keepNewline ? "\n" : " ";
512
+ out += rightTrim;
513
+ }
514
+ return out;
515
+ }
516
+ function renderLink(node, ctx) {
517
+ const label = renderInline(node.children, ctx) || node.url;
518
+ const url = node.url || "";
519
+ if (url.startsWith("mailto:")) {
520
+ // Treat mailto autolinks as plain text to avoid unwanted styling in tables.
521
+ return label;
522
+ }
523
+ if (ctx.options.hyperlinks && url) {
524
+ return osc8(url, label);
525
+ }
526
+ if (url && label !== url) {
527
+ return ctx.style(label, ctx.options.theme.link) + ctx.style(` (${url})`, { dim: true });
528
+ }
529
+ return ctx.style(label, ctx.options.theme.link);
530
+ }
531
+ function renderTable(node, ctx) {
532
+ const header = node.children[0];
533
+ if (!header)
534
+ return [];
535
+ const rows = node.children.slice(1);
536
+ const cells = [header, ...rows].map((row) => row.children.map((cell) => renderInline(cell.children, ctx)));
537
+ const colCount = Math.max(...cells.map((r) => r.length));
538
+ const widths = Array.from({ length: colCount }, () => 1);
539
+ const aligns = node.align || [];
540
+ const pad = ctx.options.tablePadding;
541
+ const padStr = " ".repeat(Math.max(0, pad));
542
+ const minContent = Math.max(1, ctx.options.tableEllipsis.length + 1);
543
+ // ensure we always have room for at least one visible char + ellipsis + padding
544
+ const minColWidth = Math.max(1, pad * 2 + minContent);
545
+ cells.forEach((row) => {
546
+ row.forEach((cell, idx) => {
547
+ const padded = `${padStr}${cell}${padStr}`;
548
+ // Cap each column to MAX_COL but keep at least 1
549
+ widths[idx] = Math.max(widths[idx] ?? 1, Math.min(MAX_COL, visibleWidth(padded)));
550
+ });
551
+ });
552
+ const totalWidth = widths.reduce((a, b) => a + b, 0) + 3 * colCount + 1;
553
+ if (ctx.options.wrap && ctx.options.width && totalWidth > ctx.options.width) {
554
+ // Shrink widest columns until the table fits; allow overflow if already at minima
555
+ let over = totalWidth - ctx.options.width;
556
+ while (over > 0) {
557
+ const i = widths.indexOf(Math.max(...widths));
558
+ if ((widths[i] ?? minColWidth) <= minColWidth)
559
+ break;
560
+ widths[i] = (widths[i] ?? minColWidth) - 1;
561
+ over -= 1;
562
+ }
563
+ }
564
+ for (let i = 0; i < widths.length; i += 1) {
565
+ if ((widths[i] ?? minColWidth) < minColWidth)
566
+ widths[i] = minColWidth;
567
+ }
568
+ const renderRow = (row, isHeader = false) => {
569
+ const linesPerCol = row.map((cell, idx) => {
570
+ const width = widths[idx] ?? minColWidth;
571
+ const target = Math.max(minContent, width - pad * 2);
572
+ const content = ctx.options.tableTruncate
573
+ ? truncateCell(cell, target, ctx.options.tableEllipsis)
574
+ : cell;
575
+ const wrapped = wrapText(content, ctx.options.wrap ? target : Number.MAX_SAFE_INTEGER, ctx.options.wrap).flatMap((line) => hardWrapTableCellLine(line, target, ctx.options.wrap));
576
+ return wrapped.map((l) => {
577
+ const aligned = padCell(l, target, aligns[idx] ?? "left");
578
+ const padded = `${padStr}${aligned}${padStr}`;
579
+ return padCell(padded, width, "left");
580
+ });
581
+ });
582
+ // Row height = max wrapped lines in any column; pad shorter ones
583
+ const height = Math.max(...linesPerCol.map((c) => c.length));
584
+ const out = [];
585
+ for (let i = 0; i < height; i += 1) {
586
+ const parts = linesPerCol.map((col, idx) => {
587
+ const content = col[i] ?? padCell("", widths[idx] ?? minColWidth, aligns[idx] ?? "left");
588
+ return isHeader
589
+ ? ctx.style(content, ctx.options.theme.tableHeader)
590
+ : ctx.style(content, ctx.options.theme.tableCell);
591
+ });
592
+ out.push(parts);
593
+ }
594
+ return out;
595
+ };
596
+ const headerRows = renderRow(header.children.map((c) => renderInline(c.children, ctx)), true);
597
+ const bodyRows = rows.map((r) => renderRow(r.children.map((c) => renderInline(c.children, ctx))));
598
+ if (ctx.options.tableBorder === "none") {
599
+ const lines = [...headerRows, ...bodyRows.flat()].map((row) => row.join(" | ")).join("\n");
600
+ return [`${lines}\n\n`];
601
+ }
602
+ const box = TABLE_BOX[ctx.options.tableBorder] || TABLE_BOX.unicode;
603
+ const hLine = (sepMid, sepLeft, sepRight) => `${sepLeft}${widths.map((w) => box.hSep.repeat(w)).join(sepMid)}${sepRight}\n`;
604
+ const top = hLine(box.tSep, box.topLeft, box.topRight);
605
+ const mid = hLine(box.mSep, box.mLeft, box.mRight);
606
+ const bottom = hLine(box.bSep, box.bottomLeft, box.bottomRight);
607
+ const renderFlat = (rowsArr) => rowsArr.map((r) => `${box.vSep}${r.map((c) => c).join(box.vSep)}${box.vSep}\n`).join("");
608
+ const dense = ctx.options.tableDense;
609
+ const renderedBody = bodyRows.map((row) => renderFlat(row)).join(dense ? "" : mid);
610
+ const out = [top, renderFlat(headerRows), dense ? "" : mid, renderedBody, bottom, "\n"];
611
+ return out;
612
+ }
613
+ function truncateCell(text, width, ellipsis) {
614
+ if (stringWidth(text) <= width)
615
+ return text;
616
+ const ellipsisWidth = stringWidth(ellipsis);
617
+ if (width <= ellipsisWidth)
618
+ return sliceCellContent(ellipsis, width);
619
+ return `${sliceCellContent(text, width - ellipsisWidth)}${ellipsis}`;
620
+ }
621
+ function sliceCellContent(text, width) {
622
+ let end = Math.max(0, width);
623
+ let sliced = sliceAnsi(text, 0, end);
624
+ // slice-ansi owns ANSI/OSC/grapheme boundaries; clamp with Markdansi's width metric.
625
+ while (end > 0 && stringWidth(sliced) > width) {
626
+ end -= 1;
627
+ sliced = sliceAnsi(text, 0, end);
628
+ }
629
+ return sliced;
630
+ }
631
+ function hardWrapTableCellLine(text, width, wrap) {
632
+ if (!wrap || width <= 0 || visibleWidth(text) <= width)
633
+ return [text];
634
+ const parts = [];
635
+ let remaining = text;
636
+ while (visibleWidth(remaining) > width) {
637
+ const part = sliceCellContent(remaining, width);
638
+ const consumedWidth = visibleWidth(part);
639
+ if (consumedWidth <= 0)
640
+ break;
641
+ parts.push(part);
642
+ remaining = sliceAnsi(remaining, consumedWidth);
643
+ }
644
+ if (remaining !== "")
645
+ parts.push(remaining);
646
+ return parts.length ? parts : [text];
647
+ }
648
+ function wrapCodeLine(text, width) {
649
+ // Hard-wrap code even without spaces while keeping ANSI-safe width accounting.
650
+ if (width <= 0)
651
+ return [text];
652
+ const parts = [];
653
+ let current = "";
654
+ for (const ch of text) {
655
+ const chWidth = stringWidth(ch);
656
+ if (visibleWidth(current) + chWidth > width) {
657
+ parts.push(current);
658
+ current = ch;
659
+ continue;
660
+ }
661
+ current += ch;
662
+ }
663
+ if (current !== "")
664
+ parts.push(current);
665
+ return parts.length ? parts : [""];
666
+ }
667
+ function padCell(text, width, align = "left", _padSpaces = 0) {
668
+ const core = text;
669
+ const pad = width - stringWidth(stripAnsi(core));
670
+ if (pad <= 0)
671
+ return core;
672
+ if (align === "right")
673
+ return `${" ".repeat(pad)}${core}`;
674
+ if (align === "center") {
675
+ const left = Math.floor(pad / 2);
676
+ const right = pad - left;
677
+ return `${" ".repeat(left)}${core}${" ".repeat(right)}`;
678
+ }
679
+ return `${core}${" ".repeat(pad)}`;
680
+ }