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,39 @@
1
+ export type MarkdownStreamerSpacing = "preserve" | "single" | "tight";
2
+ export type MarkdownStreamer = {
3
+ /**
4
+ * Push an appended Markdown delta (chunk) into the streamer.
5
+ * Returns ANSI text to write to the terminal (append-only).
6
+ */
7
+ push: (delta: string) => string;
8
+ /**
9
+ * Flush remaining buffered content and finish the stream.
10
+ * Optionally accepts one last delta.
11
+ */
12
+ finish: (finalDelta?: string) => string;
13
+ /**
14
+ * Reset internal state (buffer, fence/table detection, spacing).
15
+ */
16
+ reset: () => void;
17
+ };
18
+ export type MarkdownStreamerOptions = {
19
+ /**
20
+ * Function used to render a Markdown fragment (block or line) to ANSI.
21
+ * Must be pure (no cursor control) and must not rely on prior terminal state.
22
+ */
23
+ render: (markdown: string) => string;
24
+ /**
25
+ * Hybrid streaming: emit complete lines immediately, but buffer multi-line
26
+ * constructs (fenced code blocks + tables) until they are complete.
27
+ *
28
+ * This is designed for terminal scrollback safety: no in-place redraw, no cursor moves.
29
+ */
30
+ mode?: "hybrid";
31
+ /**
32
+ * Controls how blank lines are emitted.
33
+ * - preserve: emit blank lines exactly as received
34
+ * - single: collapse consecutive blank lines to a single blank line
35
+ * - tight: drop blank lines entirely (dense output)
36
+ */
37
+ spacing?: MarkdownStreamerSpacing;
38
+ };
39
+ export declare function createMarkdownStreamer(options: MarkdownStreamerOptions): MarkdownStreamer;
@@ -0,0 +1,192 @@
1
+ function normalizeNewlines(input) {
2
+ return input.replace(/\r\n?/g, "\n");
3
+ }
4
+ function isFenceStart(line) {
5
+ const trimmed = line.trimStart();
6
+ const match = trimmed.match(/^(```+|~~~+)/);
7
+ if (!match?.[1])
8
+ return null;
9
+ const token = match[1];
10
+ const char = token[0] === "~" ? "~" : "`";
11
+ return { char, len: token.length };
12
+ }
13
+ function isFenceEnd(line, fence) {
14
+ const trimmed = line.trimStart();
15
+ const token = fence.char.repeat(fence.len);
16
+ return trimmed.startsWith(token);
17
+ }
18
+ function looksLikeTableHeader(line) {
19
+ if (!line.includes("|"))
20
+ return false;
21
+ return /[^\s|]/.test(line);
22
+ }
23
+ function isTableSeparator(line) {
24
+ // Examples:
25
+ // | --- | --- |
26
+ // |:--- | ---:|
27
+ // --- | ---
28
+ const trimmed = line.trim();
29
+ if (!trimmed.includes("-"))
30
+ return false;
31
+ return /^\|?(?:\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?$/.test(trimmed);
32
+ }
33
+ function looksLikeTableRow(line) {
34
+ if (!line.includes("|"))
35
+ return false;
36
+ return /[^\s|]/.test(line);
37
+ }
38
+ function normalizeRenderedFragment(rendered) {
39
+ // Markdansi intentionally prefixes some blocks (e.g. headings) with a newline when rendering
40
+ // whole documents. For streaming fragments, strip leading newlines to avoid double spacing.
41
+ const trimmedStart = rendered.replace(/^\n+/, "");
42
+ // For fragment streaming, normalize to a single trailing newline so spacing is controlled
43
+ // by the streamer (blank-line collapsing) rather than renderer block heuristics.
44
+ const trimmedEnd = trimmedStart.replace(/\n+$/, "");
45
+ return `${trimmedEnd}\n`;
46
+ }
47
+ export function createMarkdownStreamer(options) {
48
+ const render = options.render;
49
+ const spacing = options.spacing ?? "single";
50
+ let buffer = "";
51
+ let blankStreak = 0;
52
+ let started = false;
53
+ let heldTableHeader = null;
54
+ let inTable = false;
55
+ let tableBuffer = "";
56
+ let fence = null;
57
+ let fenceBuffer = "";
58
+ const emitBlankLine = () => {
59
+ if (!started)
60
+ return "";
61
+ if (spacing === "tight")
62
+ return "";
63
+ if (spacing === "single" && blankStreak >= 1)
64
+ return "";
65
+ blankStreak += 1;
66
+ return "\n";
67
+ };
68
+ const emitRendered = (markdown) => {
69
+ if (!markdown)
70
+ return "";
71
+ blankStreak = 0;
72
+ started = true;
73
+ return normalizeRenderedFragment(render(markdown));
74
+ };
75
+ const flushHeldHeader = () => {
76
+ if (!heldTableHeader)
77
+ return "";
78
+ const md = heldTableHeader;
79
+ heldTableHeader = null;
80
+ return emitRendered(md);
81
+ };
82
+ const flushTable = () => {
83
+ if (!inTable)
84
+ return "";
85
+ inTable = false;
86
+ const md = tableBuffer;
87
+ tableBuffer = "";
88
+ return emitRendered(md);
89
+ };
90
+ const flushFence = () => {
91
+ if (!fence)
92
+ return "";
93
+ fence = null;
94
+ const md = fenceBuffer;
95
+ fenceBuffer = "";
96
+ return emitRendered(md);
97
+ };
98
+ const processLine = (line) => {
99
+ // Fence mode: buffer everything until the closing fence.
100
+ if (fence) {
101
+ fenceBuffer += `${line}\n`;
102
+ if (isFenceEnd(line, fence)) {
103
+ return flushFence();
104
+ }
105
+ return "";
106
+ }
107
+ // Table mode: buffer table rows; flush when it ends.
108
+ if (inTable) {
109
+ if (line.trim().length === 0) {
110
+ return flushTable() + emitBlankLine();
111
+ }
112
+ if (!looksLikeTableRow(line)) {
113
+ return flushTable() + processLine(line);
114
+ }
115
+ tableBuffer += `${line}\n`;
116
+ return "";
117
+ }
118
+ // Blank line: flush any held header and emit spacing.
119
+ if (line.trim().length === 0) {
120
+ return flushHeldHeader() + emitBlankLine();
121
+ }
122
+ // Fence start: flush held header and enter fence mode.
123
+ const fenceStart = isFenceStart(line);
124
+ if (fenceStart) {
125
+ const out = flushHeldHeader();
126
+ fence = fenceStart;
127
+ fenceBuffer = `${line}\n`;
128
+ // Some fences are single-line in streams (rare). Handle close immediately.
129
+ if (isFenceEnd(line, fenceStart) && line.trimStart().match(/^(```+|~~~+)\s*$/)) {
130
+ return out + flushFence();
131
+ }
132
+ return out;
133
+ }
134
+ // If we held a possible table header, check if this line starts a table.
135
+ if (heldTableHeader) {
136
+ if (isTableSeparator(line) && looksLikeTableHeader(heldTableHeader)) {
137
+ inTable = true;
138
+ tableBuffer = `${heldTableHeader}\n${line}\n`;
139
+ heldTableHeader = null;
140
+ return "";
141
+ }
142
+ const out = flushHeldHeader();
143
+ return out + processLine(line);
144
+ }
145
+ // Potential table header: delay emission until we see the next line.
146
+ if (looksLikeTableHeader(line)) {
147
+ heldTableHeader = line;
148
+ return "";
149
+ }
150
+ // Normal line: render immediately.
151
+ return emitRendered(line);
152
+ };
153
+ const push = (delta) => {
154
+ if (!delta)
155
+ return "";
156
+ buffer += normalizeNewlines(delta);
157
+ let out = "";
158
+ while (true) {
159
+ const idx = buffer.indexOf("\n");
160
+ if (idx < 0)
161
+ break;
162
+ const line = buffer.slice(0, idx);
163
+ buffer = buffer.slice(idx + 1);
164
+ out += processLine(line);
165
+ }
166
+ return out;
167
+ };
168
+ const finish = (finalDelta) => {
169
+ let out = "";
170
+ if (finalDelta)
171
+ out += push(finalDelta);
172
+ if (buffer.length > 0) {
173
+ out += processLine(buffer);
174
+ buffer = "";
175
+ }
176
+ out += flushHeldHeader();
177
+ out += flushFence();
178
+ out += flushTable();
179
+ return out;
180
+ };
181
+ const reset = () => {
182
+ buffer = "";
183
+ blankStreak = 0;
184
+ started = false;
185
+ heldTableHeader = null;
186
+ inTable = false;
187
+ tableBuffer = "";
188
+ fence = null;
189
+ fenceBuffer = "";
190
+ };
191
+ return { push, finish, reset };
192
+ }
@@ -0,0 +1,18 @@
1
+ import type { StyleIntent, Theme } from "./types.js";
2
+ export interface Themes {
3
+ default: Theme;
4
+ dim: Theme;
5
+ bright: Theme;
6
+ solarized: Theme;
7
+ monochrome: Theme;
8
+ contrast: Theme;
9
+ [key: string]: Theme;
10
+ }
11
+ export declare const themes: Themes;
12
+ export type Styler = (text: string, style?: StyleIntent) => string;
13
+ /**
14
+ * Create a Chalk-based styling helper that applies StyleIntent safely.
15
+ */
16
+ export declare function createStyler({ color }: {
17
+ color: boolean;
18
+ }): Styler;
@@ -0,0 +1,105 @@
1
+ import { Chalk } from "chalk";
2
+ const base = {
3
+ heading: { color: "yellow", bold: true },
4
+ strong: { bold: true },
5
+ emph: { italic: true },
6
+ inlineCode: { color: "cyan" },
7
+ blockCode: { color: "green" },
8
+ link: { color: "blue", underline: true },
9
+ quote: { dim: true },
10
+ hr: { dim: true },
11
+ listMarker: { color: "cyan" },
12
+ tableHeader: { bold: true, color: "yellow" },
13
+ tableCell: {},
14
+ };
15
+ const dim = {
16
+ ...base,
17
+ heading: { color: "white", bold: true, dim: true },
18
+ link: { color: "blue", underline: true, dim: true },
19
+ };
20
+ const bright = {
21
+ ...base,
22
+ heading: { color: "magenta", bold: true },
23
+ link: { color: "cyan", underline: true },
24
+ inlineCode: { color: "green" },
25
+ blockCode: { color: "green" },
26
+ };
27
+ const solarized = {
28
+ heading: { color: "yellow", bold: true },
29
+ strong: { bold: true },
30
+ emph: { italic: true },
31
+ inlineCode: { color: "cyan" },
32
+ blockCode: { color: "#2aa198" },
33
+ link: { color: "blue", underline: true },
34
+ quote: { color: "white", dim: true },
35
+ hr: { color: "white", dim: true },
36
+ listMarker: { color: "cyan" },
37
+ tableHeader: { color: "yellow", bold: true },
38
+ };
39
+ const monochrome = {
40
+ heading: { bold: true },
41
+ strong: { bold: true },
42
+ emph: { italic: true },
43
+ inlineCode: { dim: true },
44
+ blockCode: { dim: true },
45
+ link: { underline: true },
46
+ quote: { dim: true },
47
+ hr: { dim: true },
48
+ listMarker: { dim: true },
49
+ tableHeader: { bold: true },
50
+ };
51
+ const contrast = {
52
+ heading: { color: "magenta", bold: true },
53
+ strong: { color: "white", bold: true },
54
+ emph: { color: "white", italic: true },
55
+ inlineCode: { color: "cyan", bold: true },
56
+ blockCode: { color: "green", bold: true },
57
+ link: { color: "blue", underline: true },
58
+ quote: { color: "white", dim: true },
59
+ hr: { color: "white", dim: true },
60
+ listMarker: { color: "yellow", bold: true },
61
+ tableHeader: { color: "yellow", bold: true },
62
+ tableCell: { color: "white" },
63
+ };
64
+ export const themes = {
65
+ default: Object.freeze(base),
66
+ dim: Object.freeze(dim),
67
+ bright: Object.freeze(bright),
68
+ solarized: Object.freeze(solarized),
69
+ monochrome: Object.freeze(monochrome),
70
+ contrast: Object.freeze(contrast),
71
+ };
72
+ /**
73
+ * Create a Chalk-based styling helper that applies StyleIntent safely.
74
+ */
75
+ export function createStyler({ color }) {
76
+ const level = color ? 3 : 0;
77
+ const chalk = new Chalk({ level });
78
+ const apply = (text, style = {}) => {
79
+ if (!color)
80
+ return text;
81
+ let fn = chalk;
82
+ if (style.color) {
83
+ const indexed = fn;
84
+ if (indexed[style.color])
85
+ fn = indexed[style.color];
86
+ }
87
+ if (style.bgColor) {
88
+ const indexed = fn;
89
+ if (indexed[style.bgColor])
90
+ fn = indexed[style.bgColor];
91
+ }
92
+ if (style.bold)
93
+ fn = fn.bold;
94
+ if (style.italic)
95
+ fn = fn.italic;
96
+ if (style.underline)
97
+ fn = fn.underline;
98
+ if (style.dim)
99
+ fn = fn.dim;
100
+ if (style.strike)
101
+ fn = fn.strikethrough;
102
+ return fn(text);
103
+ };
104
+ return apply;
105
+ }
@@ -0,0 +1,58 @@
1
+ export type ColorName = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | `#${string}` | `${number}`;
2
+ export type StyleIntent = {
3
+ color?: ColorName;
4
+ bgColor?: ColorName;
5
+ bold?: boolean;
6
+ italic?: boolean;
7
+ underline?: boolean;
8
+ dim?: boolean;
9
+ strike?: boolean;
10
+ };
11
+ export type Theme = {
12
+ heading?: StyleIntent;
13
+ strong?: StyleIntent;
14
+ emph?: StyleIntent;
15
+ inlineCode?: StyleIntent;
16
+ blockCode?: StyleIntent;
17
+ code?: StyleIntent;
18
+ link?: StyleIntent;
19
+ quote?: StyleIntent;
20
+ hr?: StyleIntent;
21
+ listMarker?: StyleIntent;
22
+ tableHeader?: StyleIntent;
23
+ tableCell?: StyleIntent;
24
+ };
25
+ export type ThemeName = "default" | "dim" | "bright";
26
+ export type Highlighter = (code: string, lang?: string) => string;
27
+ export interface RenderOptions {
28
+ wrap?: boolean;
29
+ width?: number;
30
+ hyperlinks?: boolean;
31
+ color?: boolean;
32
+ theme?: ThemeName | Theme;
33
+ /**
34
+ * Spaces per nesting level for lists (default 2).
35
+ */
36
+ listIndent?: number;
37
+ /**
38
+ * Prefix used for blockquotes (default "│ ").
39
+ */
40
+ quotePrefix?: string;
41
+ /** Table border style: unicode (default), ascii, or none. */
42
+ tableBorder?: "unicode" | "ascii" | "none";
43
+ /** Spaces around cell content (default 1). */
44
+ tablePadding?: number;
45
+ /** If true, reduces separator rows (default false). */
46
+ tableDense?: boolean;
47
+ /** If true, truncates cell content to fit column width (default true). */
48
+ tableTruncate?: boolean;
49
+ /** Ellipsis text for truncation (default "…"). */
50
+ tableEllipsis?: string;
51
+ /** Draw a box around fenced code blocks (default true). */
52
+ codeBox?: boolean;
53
+ /** Show line-number gutter for code blocks (default false). */
54
+ codeGutter?: boolean;
55
+ /** Wrap code lines to width; otherwise overflow (default true). */
56
+ codeWrap?: boolean;
57
+ highlighter?: Highlighter;
58
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Visible width of a string, ignoring ANSI escape codes.
3
+ */
4
+ export declare function visibleWidth(text: string): number;
5
+ /**
6
+ * Wrap a single paragraph string into lines respecting visible width.
7
+ * Breaks only on spaces. Words longer than width overflow.
8
+ */
9
+ export declare function wrapText(text: string, width: number, wrap: boolean): string[];
10
+ export declare function wrapWithPrefix(text: string, width: number, wrap: boolean, prefix?: string): string[];
@@ -0,0 +1,73 @@
1
+ import stringWidth from "string-width";
2
+ import stripAnsi from "strip-ansi";
3
+ /**
4
+ * Visible width of a string, ignoring ANSI escape codes.
5
+ */
6
+ export function visibleWidth(text) {
7
+ return stringWidth(stripAnsi(text));
8
+ }
9
+ /**
10
+ * Wrap a single paragraph string into lines respecting visible width.
11
+ * Breaks only on spaces. Words longer than width overflow.
12
+ */
13
+ export function wrapText(text, width, wrap) {
14
+ if (!wrap || width <= 0)
15
+ return [text];
16
+ const words = text.split(/(\s+)/).filter((w) => w.length > 0);
17
+ const lines = [];
18
+ let current = "";
19
+ let currentWidth = 0;
20
+ const trimEndSpaces = (s) => s.replace(/\s+$/, "");
21
+ const orphanPhraseTail = (s) => {
22
+ const trimmed = trimEndSpaces(s);
23
+ const phrase = trimmed.match(/\b(with|in|on|of|to|for)\s+(a|an|the)$/i);
24
+ if (phrase) {
25
+ const preposition = phrase[1];
26
+ const article = phrase[2];
27
+ if (preposition && article)
28
+ return `${preposition} ${article}`;
29
+ }
30
+ const single = trimmed.match(/\b(a|an|the|to|of|with|and|or|in|on|for)$/i);
31
+ return single?.[1] ?? null;
32
+ };
33
+ for (const word of words) {
34
+ const w = visibleWidth(word);
35
+ if (current !== "" && currentWidth + w > width && !/^\s+$/.test(word)) {
36
+ const nextWord = word.replace(/^\s+/, "");
37
+ const currentNoTrail = trimEndSpaces(current);
38
+ const tail = orphanPhraseTail(currentNoTrail);
39
+ if (tail && currentNoTrail.length > tail.length) {
40
+ const base = trimEndSpaces(currentNoTrail.slice(0, currentNoTrail.length - tail.length));
41
+ if (base !== "") {
42
+ lines.push(base);
43
+ current = `${tail} ${nextWord}`;
44
+ currentWidth = visibleWidth(current);
45
+ continue;
46
+ }
47
+ }
48
+ lines.push(currentNoTrail);
49
+ current = nextWord;
50
+ currentWidth = visibleWidth(current);
51
+ continue;
52
+ }
53
+ current += word;
54
+ currentWidth = visibleWidth(current);
55
+ }
56
+ if (current !== "")
57
+ lines.push(trimEndSpaces(current));
58
+ if (lines.length === 0)
59
+ lines.push("");
60
+ return lines;
61
+ }
62
+ export function wrapWithPrefix(text, width, wrap, prefix = "") {
63
+ if (!wrap)
64
+ return text.split("\n").map((line) => prefix + line);
65
+ const out = [];
66
+ const w = Math.max(1, width - visibleWidth(prefix));
67
+ for (const line of text.split("\n")) {
68
+ const parts = wrapText(line, w, wrap);
69
+ for (const p of parts)
70
+ out.push(prefix + p);
71
+ }
72
+ return out;
73
+ }
@@ -0,0 +1,110 @@
1
+ # Markdansi v0.1.2 – Design Spec
2
+
3
+ Goal: Tiny, dependency‑light Markdown → ANSI renderer & CLI for Node ≥22, using pnpm. Output is terminal ANSI only (no HTML). Focus on readable defaults, sensible wrapping, and minimal runtime deps.
4
+
5
+ ## Core Dependencies (runtime)
6
+
7
+ - `marked`: GFM parsing (tables, task lists, strikethrough, autolink literals).
8
+ - `decode-named-character-reference`: decode Markdown character references in text and URLs.
9
+ - `chalk`: small, ESM‑only color/style helper.
10
+ - `string-width`: correct visible width (emoji / wide chars).
11
+ - `strip-ansi`: strip codes for width/wrapping.
12
+ - `supports-hyperlinks`: detect OSC‑8 hyperlink support.
13
+
14
+ Dev: `vitest`, TypeScript (NodeNext).
15
+
16
+ ## Surface Area
17
+
18
+ ### Library (ESM default, CJS export provided)
19
+
20
+ `render(markdown: string, options?: RenderOptions): string`
21
+
22
+ `createRenderer(options?: RenderOptions): (md: string) => string`
23
+
24
+ `type RenderOptions = {`
25
+ ` wrap?: boolean; // default: true; if false => no hard wraps anywhere`
26
+ ` width?: number; // used only when wrap===true; default: TTY cols or 80`
27
+ ` hyperlinks?: boolean; // default: auto via supports-hyperlinks`
28
+ ` color?: boolean; // default: true if TTY; if false => no ANSI/OSC at all`
29
+ ` theme?: ThemeName | Theme; // built-ins: default, dim, bright, solarized, monochrome, contrast`
30
+ ` listIndent?: number; // spaces per nesting level; default 2`
31
+ ` quotePrefix?: string; // blockquote line prefix; default "│ "`
32
+ ` tableBorder?: "unicode" | "ascii" | "none"; // default unicode box drawing`
33
+ ` tablePadding?: number; // spaces inside cells (L/R); default 1`
34
+ ` tableDense?: boolean; // reduce separator rows; default false`
35
+ ` tableTruncate?: boolean; // truncate cells to fit col widths; default true`
36
+ ` tableEllipsis?: string; // truncation marker; default "…"`
37
+ ` codeBox?: boolean; // draw a box around fenced code; default true`
38
+ ` codeGutter?: boolean; // left gutter with line numbers; default false`
39
+ ` codeWrap?: boolean; // wrap code to width; default true`
40
+ ` highlighter?: (code: string, lang?: string) => string; // hook, must not add newlines`
41
+ `}``
42
+
43
+ `type Theme = { heading, strong, emph, inlineCode, blockCode, code?, link, quote, hr, listMarker, tableHeader, tableCell, tableBorder, tableSeparator }`
44
+ Each theme entry holds simple SGR intents (bold/italic/fg color names). `inlineCode` / `blockCode` are used if present; otherwise `code` acts as a fallback for both. Theme exposes defaults for table borders/separators; caller can override per render via options above.
45
+
46
+ `strip(markdown: string): string` — convenience: render with `color=false`, `hyperlinks=false`.
47
+
48
+ ### CLI
49
+
50
+ `markdansi [FILE] [--in FILE] [--out FILE] [--width N] [--no-wrap] [--no-color] [--no-links] [--theme default|dim|bright]`
51
+
52
+ - Input: positional `FILE`, `--in FILE`, or stdin when neither is given.
53
+ - Output: stdout if no `--out`.
54
+ - Wrap: on by default; `--no-wrap` disables; width auto from TTY when not provided.
55
+ - Links: OSC‑8 hyperlinks enabled when terminal supports; `--no-links` disables.
56
+
57
+ ## Feature Scope (v1)
58
+
59
+ - Blocks: paragraphs, headings (1–6), blockquotes, fenced/indented code blocks, HR, tables, unordered/ordered lists, task lists.
60
+ - Inline: strong, emphasis, code spans, autolinks/links, strikethrough (GFM `~~`), backslash escapes.
61
+ - Code blocks: monospace box (unicode or ascii; `codeBox=false` disables). Optional gutter with 1‑based line numbers when `codeGutter=true`. If `lang` present, show faint header label. Highlighter hook may recolor text but must not add/remove newlines. Code blocks wrap to the available width by default (hard-wrap long tokens); set `codeWrap=false` to allow overflow.
62
+ - Tables: box-drawing (unicode default, ascii or none). Respect GFM alignment per column, pad cells by `tablePadding`, optional dense borders. Can truncate cell text (`tableTruncate=true`, `tableEllipsis` marker) to keep width. Width balancing shrinks columns while possible; if still too wide, cells overflow.
63
+ - Wrapping: word-wrap on spaces; uses `string-width` on stripped text. Preserve hard breaks; words longer than width may overflow. Code blocks wrap by default; turn off with `codeWrap=false`.
64
+ - Hyperlinks: OSC‑8 when supported and allowed; fallback to underlined text plus URL in parentheses.
65
+ - Error handling: never throw on malformed emphasis; leave literals untouched if unmatched.
66
+
67
+ ## Rendering Pipeline
68
+
69
+ 1. **Parse** via Marked's GFM lexer → lightweight internal AST.
70
+ 2. **Build light IR** (nodes: paragraph, heading, list, listItem, taskItem, table, tableRow, tableCell, code, inline text/emph/strong/del/code/link).
71
+ 3. **Render** to ANSI:
72
+ - Style map from theme to SGR codes.
73
+ - Wrap paragraphs/table cells using `string-width` + `strip-ansi`; wrap only breaks on spaces.
74
+ - OSC‑8 links when `hyperlinks` true; otherwise underline + optional URL suffix.
75
+ - Track active SGR for wrapping splits to re-open styles on new lines.
76
+
77
+ ## Themes (initial)
78
+
79
+ - `default`: bold headings, blue links, cyan inline code, green block code, yellow table headers, subtle quotes/hr.
80
+ - `dim`: muted colors for low-contrast terminals.
81
+ - `bright`: higher contrast variant.
82
+ - `solarized`: yellow headings, cyan inline, teal block code, blue links, yellow headers.
83
+ - `monochrome`: bold/italic cues only, dim code, underlined links.
84
+ - `contrast`: magenta headings, cyan inline, green block code, yellow headers, bright markers.
85
+
86
+ ## Testing (vitest)
87
+
88
+ - Unit: inline formatting (emph/strong/code/strike), links/hyperlinks on/off, wrap/no-wrap behavior, table alignment and wrapping, task lists, strikethrough.
89
+ - Snapshot-ish string comparisons for representative documents (with colors off to avoid brittle codes).
90
+
91
+ ## Non-Goals (v1)
92
+
93
+ - Images, footnotes, math, HTML passthrough, syntax highlighting bundle.
94
+
95
+ ## Notes
96
+
97
+ - Highlighting: built-in is “label-only”; extensibility via `highlighter` hook. No extra deps added for highlighting.
98
+ - ESM-first; provide CJS export entry for compatibility.
99
+
100
+ ## Behaviors & edge-case rules
101
+
102
+ - Wrap/width precedence: `wrap=false` disables all hard wrapping; `width` is ignored in that mode. When `wrap=true`, width is `options.width ?? ttyColumns ?? 80`.
103
+ - Color flag: `color=false` removes all ANSI/OSC output (no bold/italic/underline, no hyperlinks); output is plain text.
104
+ - Hyperlinks fallback: inline links render as `label (url)` when OSC‑8 disabled; autolinks render as the URL only. URLs count toward width.
105
+ - Highlighter hook: receives raw code and optional lang; may return ANSI-colored text but must not add or remove newlines. Markdansi owns indentation/padding; code blocks never hard-wrap.
106
+ - Tables width algorithm: compute desired column widths from content (cap at e.g. 40). While total exceeds width, decrement widest columns until it fits; if even minimums won’t fit, allow overflow. Respect GFM alignment per column. Cells with newlines keep those breaks. Optional truncation shortens cells before layout with `tableEllipsis`.
107
+ - Lists: honor GFM tight vs loose lists (tight => no blank line between items; loose => blank line). Nesting indent = 2 spaces per level; bullets use `-`; ordered lists use input numbering.
108
+ - Blockquotes: prefix each wrapped line with `│ ` (configurable via `quotePrefix`); quote content wraps accounting for the prefix width.
109
+ - List indent is configurable via `listIndent` (default 2 spaces per level).
110
+ - Reference-style definitions with indented title continuations are merged into a single paragraph (instead of becoming indented code blocks), preventing stray boxed output in copied logs.