@minko-ai/minko 0.1.0 → 0.1.1

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 (189) hide show
  1. package/README.md +577 -2
  2. package/assets/mascot-preview.png +0 -0
  3. package/assets/mascot-preview.txt +46 -0
  4. package/assets/mascot-source.png +0 -0
  5. package/assets/skills/README.md +21 -0
  6. package/assets/skills/brainstorming/LICENSE +21 -0
  7. package/assets/skills/brainstorming/SKILL.md +316 -0
  8. package/assets/skills/buddhist-method/LICENSE +21 -0
  9. package/assets/skills/buddhist-method/SKILL.md +137 -0
  10. package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
  11. package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
  12. package/assets/skills/code-humanizer/LICENSE +21 -0
  13. package/assets/skills/code-humanizer/SKILL.md +174 -0
  14. package/assets/skills/code-review/LICENSE +21 -0
  15. package/assets/skills/code-review/SKILL.md +232 -0
  16. package/assets/skills/code-review/reference/angular.md +768 -0
  17. package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
  18. package/assets/skills/code-review/reference/c.md +890 -0
  19. package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
  20. package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
  21. package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
  22. package/assets/skills/code-review/reference/cpp.md +893 -0
  23. package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  24. package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  25. package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  26. package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  27. package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  28. package/assets/skills/code-review/reference/csharp.md +525 -0
  29. package/assets/skills/code-review/reference/css-less-sass.md +661 -0
  30. package/assets/skills/code-review/reference/dart.md +670 -0
  31. package/assets/skills/code-review/reference/django.md +985 -0
  32. package/assets/skills/code-review/reference/fastapi.md +580 -0
  33. package/assets/skills/code-review/reference/go.md +993 -0
  34. package/assets/skills/code-review/reference/java.md +409 -0
  35. package/assets/skills/code-review/reference/java8.md +586 -0
  36. package/assets/skills/code-review/reference/kotlin.md +1018 -0
  37. package/assets/skills/code-review/reference/nestjs.md +593 -0
  38. package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
  39. package/assets/skills/code-review/reference/php.md +684 -0
  40. package/assets/skills/code-review/reference/python.md +1073 -0
  41. package/assets/skills/code-review/reference/qt.md +757 -0
  42. package/assets/skills/code-review/reference/react.md +871 -0
  43. package/assets/skills/code-review/reference/ruby.md +964 -0
  44. package/assets/skills/code-review/reference/rust.md +846 -0
  45. package/assets/skills/code-review/reference/security-review-guide.md +494 -0
  46. package/assets/skills/code-review/reference/svelte.md +1064 -0
  47. package/assets/skills/code-review/reference/swift.md +936 -0
  48. package/assets/skills/code-review/reference/typescript.md +1016 -0
  49. package/assets/skills/code-review/reference/vue.md +924 -0
  50. package/assets/skills/code-review/reference/zig.md +440 -0
  51. package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
  52. package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
  53. package/assets/skills/five-whys/LICENSE +21 -0
  54. package/assets/skills/five-whys/SKILL.md +247 -0
  55. package/assets/skills/five-whys/examples/performance-regression.md +161 -0
  56. package/assets/skills/five-whys/examples/production-incident.md +134 -0
  57. package/assets/skills/five-whys/references/software-patterns.md +284 -0
  58. package/assets/skills/five-whys/references/toyota-origins.md +257 -0
  59. package/assets/skills/systematic-debugging/LICENSE +21 -0
  60. package/assets/skills/systematic-debugging/SKILL.md +283 -0
  61. package/assets/skills/test-driven-development/LICENSE +21 -0
  62. package/assets/skills/test-driven-development/SKILL.md +330 -0
  63. package/assets/skills/verification-before-completion/LICENSE +21 -0
  64. package/assets/skills/verification-before-completion/SKILL.md +120 -0
  65. package/assets/skills/vibe-sec/LICENSE +201 -0
  66. package/assets/skills/vibe-sec/SKILL.md +758 -0
  67. package/assets/skills/writing-plans/LICENSE +21 -0
  68. package/assets/skills/writing-plans/SKILL.md +204 -0
  69. package/bin/minko.js +2 -2
  70. package/dist/agent/approvalTokens.js +224 -0
  71. package/dist/agent/branchLock.js +315 -0
  72. package/dist/agent/compact.js +182 -0
  73. package/dist/agent/context.js +183 -0
  74. package/dist/agent/cron.js +326 -0
  75. package/dist/agent/hooks.js +211 -0
  76. package/dist/agent/lanes.js +511 -0
  77. package/dist/agent/loop.js +337 -0
  78. package/dist/agent/memory.js +159 -0
  79. package/dist/agent/permissions.js +71 -0
  80. package/dist/agent/plan.js +118 -0
  81. package/dist/agent/prompt.js +120 -0
  82. package/dist/agent/subagent.js +67 -0
  83. package/dist/agent/tasks.js +449 -0
  84. package/dist/agent/transcript.js +62 -0
  85. package/dist/agent/workerStore.js +172 -0
  86. package/dist/agent/workerTypes.js +1 -0
  87. package/dist/agent/workers.js +503 -0
  88. package/dist/cli/app.js +261 -0
  89. package/dist/cli/argv.js +206 -0
  90. package/dist/cli/commandsCommand.js +12 -0
  91. package/dist/cli/configCommand.js +170 -0
  92. package/dist/cli/doctorCommand.js +112 -0
  93. package/dist/cli/exportCommand.js +85 -0
  94. package/dist/cli/forkCommand.js +24 -0
  95. package/dist/cli/linkCommand.js +272 -0
  96. package/dist/cli/loginCommand.js +69 -0
  97. package/dist/cli/mcpLoginCommand.js +68 -0
  98. package/dist/cli/pluginCommand.js +113 -0
  99. package/dist/cli/powerCommand.js +174 -0
  100. package/dist/cli/promptFooter.js +74 -0
  101. package/dist/cli/relayCommand.js +38 -0
  102. package/dist/cli/sandboxCommand.js +31 -0
  103. package/dist/cli/serveCommand.js +104 -0
  104. package/dist/cli/statusCommand.js +120 -0
  105. package/dist/cli/tui.js +1104 -0
  106. package/dist/commands/registry.js +99 -0
  107. package/dist/config/paths.js +81 -0
  108. package/dist/config/store.js +605 -0
  109. package/dist/index.js +199 -0
  110. package/dist/link/protocol.js +159 -0
  111. package/dist/link/relay.js +184 -0
  112. package/dist/link/server.js +566 -0
  113. package/dist/link/tunnel.js +90 -0
  114. package/dist/lsp/client.js +340 -0
  115. package/dist/lsp/tool.js +177 -0
  116. package/dist/mcp/client.js +469 -0
  117. package/dist/mcp/httpClient.js +191 -0
  118. package/dist/mcp/server.js +206 -0
  119. package/dist/mcp/tools.js +173 -0
  120. package/dist/oauth/device.js +111 -0
  121. package/dist/oauth/store.js +56 -0
  122. package/dist/plugins/loader.js +291 -0
  123. package/dist/plugins/market.js +98 -0
  124. package/dist/providers/anthropic.js +279 -0
  125. package/dist/providers/factory.js +109 -0
  126. package/dist/providers/openai.js +182 -0
  127. package/dist/providers/retry.js +95 -0
  128. package/dist/providers/sse.js +67 -0
  129. package/dist/providers/types.js +10 -0
  130. package/dist/rag/embeddings.js +171 -0
  131. package/dist/rag/index.js +373 -0
  132. package/dist/rag/server.js +167 -0
  133. package/dist/rag/tool.js +148 -0
  134. package/dist/sandbox/index.js +55 -0
  135. package/dist/sdk/server.js +150 -0
  136. package/dist/sdk/session.js +104 -0
  137. package/dist/session/liveness.js +148 -0
  138. package/dist/session/recorder.js +49 -0
  139. package/dist/session/store.js +171 -0
  140. package/dist/skills/index.js +428 -0
  141. package/dist/theme.js +30 -0
  142. package/dist/tools/fs.js +195 -0
  143. package/dist/tools/notebook.js +109 -0
  144. package/dist/tools/registry.js +32 -0
  145. package/dist/tools/search.js +130 -0
  146. package/dist/tools/shell.js +200 -0
  147. package/dist/tools/structured.js +83 -0
  148. package/dist/tools/types.js +1 -0
  149. package/dist/ui/ansi.js +135 -0
  150. package/dist/ui/banner.js +103 -0
  151. package/dist/ui/input.js +49 -0
  152. package/dist/ui/inputModel.js +113 -0
  153. package/dist/ui/keyboard.js +142 -0
  154. package/dist/ui/mascot.js +34 -0
  155. package/dist/ui/mascotData.js +96 -0
  156. package/dist/ui/md.js +183 -0
  157. package/dist/ui/screen.js +49 -0
  158. package/dist/ui/spinner.js +52 -0
  159. package/dist/ui/transcript.js +287 -0
  160. package/dist/util/clipboard.js +53 -0
  161. package/dist/util/errors.js +50 -0
  162. package/dist/util/version.js +12 -0
  163. package/dist/web/fetch.js +168 -0
  164. package/dist/web/search.js +97 -0
  165. package/package.json +45 -36
  166. package/src/agent/loop.ts +0 -159
  167. package/src/agent/prompt.ts +0 -45
  168. package/src/cli/app.ts +0 -396
  169. package/src/cli/argv.ts +0 -139
  170. package/src/cli/configCommand.ts +0 -175
  171. package/src/config/paths.ts +0 -20
  172. package/src/config/store.ts +0 -149
  173. package/src/index.ts +0 -120
  174. package/src/providers/anthropic.ts +0 -215
  175. package/src/providers/openai.ts +0 -158
  176. package/src/providers/sse.ts +0 -57
  177. package/src/providers/types.ts +0 -59
  178. package/src/session/store.ts +0 -125
  179. package/src/theme.ts +0 -41
  180. package/src/tools/fs.ts +0 -143
  181. package/src/tools/registry.ts +0 -19
  182. package/src/tools/shell.ts +0 -120
  183. package/src/tools/types.ts +0 -26
  184. package/src/ui/ansi.ts +0 -110
  185. package/src/ui/banner.ts +0 -39
  186. package/src/ui/input.ts +0 -54
  187. package/src/ui/md.ts +0 -160
  188. package/src/ui/spinner.ts +0 -58
  189. package/src/util/errors.ts +0 -48
@@ -0,0 +1,135 @@
1
+ import { colorEnabled, PURPLE } from "../theme.js";
2
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
3
+ export function hexToRgb(hex) {
4
+ const raw = hex.replace('#', '');
5
+ const full = raw.length === 3 ? raw.split('').map((c) => c + c).join('') : raw;
6
+ const value = parseInt(full, 16);
7
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
8
+ }
9
+ /** A small painter: every method is a no-op when colour is disabled. */
10
+ export class Painter {
11
+ enabled;
12
+ constructor(enabled = colorEnabled()) {
13
+ this.enabled = enabled;
14
+ }
15
+ fg(hex, text) {
16
+ if (!this.enabled) {
17
+ return text;
18
+ }
19
+ const [r, g, b] = hexToRgb(hex);
20
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
21
+ }
22
+ bg(hex, text) {
23
+ if (!this.enabled) {
24
+ return text;
25
+ }
26
+ const [r, g, b] = hexToRgb(hex);
27
+ return `\x1b[48;2;${r};${g};${b}m${text}\x1b[49m`;
28
+ }
29
+ bold(text) {
30
+ if (!this.enabled) {
31
+ return text;
32
+ }
33
+ return `\x1b[1m${text}\x1b[22m`;
34
+ }
35
+ dim(text) {
36
+ if (!this.enabled) {
37
+ return text;
38
+ }
39
+ return `\x1b[2m${text}\x1b[22m`;
40
+ }
41
+ italic(text) {
42
+ if (!this.enabled) {
43
+ return text;
44
+ }
45
+ return `\x1b[3m${text}\x1b[23m`;
46
+ }
47
+ underline(text) {
48
+ if (!this.enabled) {
49
+ return text;
50
+ }
51
+ return `\x1b[4m${text}\x1b[24m`;
52
+ }
53
+ primary(text) { return this.fg(PURPLE.primary, text); }
54
+ soft(text) { return this.fg(PURPLE.soft, text); }
55
+ deep(text) { return this.fg(PURPLE.deep, text); }
56
+ muted(text) { return this.fg(PURPLE.muted, text); }
57
+ dimmed(text) { return this.dim(this.fg(PURPLE.dim, text)); }
58
+ error(text) { return this.fg(PURPLE.error, text); }
59
+ ok(text) { return this.fg(PURPLE.ok, text); }
60
+ warn(text) { return this.fg(PURPLE.warn, text); }
61
+ }
62
+ export function stripAnsi(text) {
63
+ return text.replace(ANSI_PATTERN, '');
64
+ }
65
+ /** Terminal columns occupied by a string (CJK and emoji count double). */
66
+ export function visibleWidth(text) {
67
+ let width = 0;
68
+ for (const char of stripAnsi(text)) {
69
+ width += charWidth(char);
70
+ }
71
+ return width;
72
+ }
73
+ export function charWidth(char) {
74
+ const code = char.codePointAt(0) ?? 0;
75
+ return isWide(code) ? 2 : 1;
76
+ }
77
+ /** Splits plain text into lines no wider than `width` terminal columns. */
78
+ export function wrapText(text, width) {
79
+ if (width <= 4 || visibleWidth(text) <= width) {
80
+ return [text];
81
+ }
82
+ const lines = [];
83
+ let current = '';
84
+ let currentWidth = 0;
85
+ for (const char of text) {
86
+ const w = charWidth(char);
87
+ if (currentWidth + w > width && current.length > 0) {
88
+ lines.push(current);
89
+ current = '';
90
+ currentWidth = 0;
91
+ }
92
+ current += char;
93
+ currentWidth += w;
94
+ }
95
+ if (current.length > 0) {
96
+ lines.push(current);
97
+ }
98
+ return lines;
99
+ }
100
+ function isWide(code) {
101
+ return (code >= 0x1100 && code <= 0x115f)
102
+ || (code >= 0x2e80 && code <= 0xa4cf)
103
+ || (code >= 0xac00 && code <= 0xd7a3)
104
+ || (code >= 0xf900 && code <= 0xfaff)
105
+ || (code >= 0xfe30 && code <= 0xfe4f)
106
+ || (code >= 0xff00 && code <= 0xff60)
107
+ || (code >= 0xffe0 && code <= 0xffe6)
108
+ || (code >= 0x1f300 && code <= 0x1faff)
109
+ || (code >= 0x20000 && code <= 0x3fffd);
110
+ }
111
+ export function truncateVisible(text, max) {
112
+ if (visibleWidth(text) <= max) {
113
+ return text;
114
+ }
115
+ let out = '';
116
+ let width = 0;
117
+ for (const char of stripAnsi(text)) {
118
+ const code = char.codePointAt(0) ?? 0;
119
+ const w = isWide(code) ? 2 : 1;
120
+ if (width + w > max - 1) {
121
+ break;
122
+ }
123
+ out += char;
124
+ width += w;
125
+ }
126
+ return out + '…';
127
+ }
128
+ export function terminalWidth(fallback = 80) {
129
+ const columns = process.stdout.columns;
130
+ return typeof columns === 'number' && columns > 20 ? columns : fallback;
131
+ }
132
+ /** Text with an even two-column grid, used by the banner. */
133
+ export function spaced(text, gap = ' ') {
134
+ return text.split('').join(gap);
135
+ }
@@ -0,0 +1,103 @@
1
+ import { terminalWidth, visibleWidth } from "./ansi.js";
2
+ import { mascotFrames, MASCOT_ROWS } from "./mascot.js";
3
+ import { MASCOT_WIDTH } from "./mascotData.js";
4
+ import { PURPLE } from "../theme.js";
5
+ const GAP = ' ';
6
+ const MIN_BOX = 36;
7
+ const MAX_BOX = 72;
8
+ export function bannerLines(painter, info, frame) {
9
+ const content = [
10
+ painter.bold(painter.soft('Minko')) + ' ' + painter.dimmed('v' + info.version),
11
+ '',
12
+ painter.primary(info.model),
13
+ painter.muted(info.cwd),
14
+ ];
15
+ const widest = content.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
16
+ const boxWidth = Math.min(MAX_BOX, Math.max(MIN_BOX, widest + 4));
17
+ const box = boxLines(painter, content, boxWidth);
18
+ if (!frame) {
19
+ return box.map((line) => ' ' + line);
20
+ }
21
+ const rows = Math.max(MASCOT_ROWS, box.length);
22
+ const padTop = Math.max(0, Math.floor((rows - box.length) / 2));
23
+ const lines = [];
24
+ for (let index = 0; index < rows; index += 1) {
25
+ const left = frame[index] ?? '';
26
+ const padded = left + ' '.repeat(Math.max(0, MASCOT_WIDTH - visibleWidth(left)));
27
+ const boxIndex = index - padTop;
28
+ const right = boxIndex >= 0 && boxIndex < box.length ? box[boxIndex] : '';
29
+ lines.push(' ' + padded + GAP + right);
30
+ }
31
+ return lines;
32
+ }
33
+ function boxLines(painter, content, width) {
34
+ const border = (text) => painter.fg(PURPLE.border, text);
35
+ const inner = width - 4;
36
+ const lines = [border('╭' + '─'.repeat(width - 2) + '╮')];
37
+ for (const line of content) {
38
+ const padding = ' '.repeat(Math.max(0, inner - visibleWidth(line)));
39
+ lines.push(border('│') + ' ' + line + padding + ' ' + border('│'));
40
+ }
41
+ lines.push(border('╰' + '─'.repeat(width - 2) + '╯'));
42
+ return lines;
43
+ }
44
+ /** Two-line header for terminals too short for the full welcome block. */
45
+ export function compactBanner(painter, info) {
46
+ return [
47
+ ' ' + painter.bold(painter.soft('Minko')) + ' ' + painter.dimmed('v' + info.version)
48
+ + painter.dimmed(' · ' + info.model),
49
+ '',
50
+ ];
51
+ }
52
+ /** Draws the welcome block, animating the mascot in place when allowed. */
53
+ export async function printBanner(painter, info) {
54
+ const animated = Boolean(process.stdout.isTTY) && painter.enabled
55
+ && process.env.MINKO_NO_ANIMATION !== '1';
56
+ const allowMascot = process.env.MINKO_MASCOT !== '0';
57
+ const frames = allowMascot && terminalWidth() >= MASCOT_WIDTH + 6
58
+ ? mascotFrames(painter)
59
+ : [];
60
+ const sideBySide = terminalWidth() - (MASCOT_WIDTH + GAP.length + 4) >= MIN_BOX;
61
+ const compose = (index) => {
62
+ if (frames.length === 0) {
63
+ return bannerLines(painter, info, null);
64
+ }
65
+ if (sideBySide) {
66
+ return bannerLines(painter, info, frames[index]);
67
+ }
68
+ return [...frames[index].map((line) => ' ' + line), '', ...bannerLines(painter, info, null)];
69
+ };
70
+ process.stdout.write('\n' + compose(0).join('\n') + '\n');
71
+ if (!animated || frames.length === 0) {
72
+ process.stdout.write('\n');
73
+ return;
74
+ }
75
+ const plan = [[1, 480], [2, 140], [3, 700], [4, 520]];
76
+ const rows = compose(0).length;
77
+ for (const [index, delay] of plan) {
78
+ if (index >= frames.length) {
79
+ continue;
80
+ }
81
+ await sleep(delay);
82
+ process.stdout.write('\x1b[' + rows + 'A');
83
+ for (const line of compose(index)) {
84
+ process.stdout.write('\x1b[2K' + line + '\n');
85
+ }
86
+ }
87
+ process.stdout.write('\n');
88
+ }
89
+ function sleep(ms) {
90
+ return new Promise((resolve) => setTimeout(resolve, ms));
91
+ }
92
+ export function formatTokens(value) {
93
+ if (value < 1000) {
94
+ return String(value);
95
+ }
96
+ return (value / 1000).toFixed(1) + 'k';
97
+ }
98
+ export function formatDuration(ms) {
99
+ if (ms < 1000) {
100
+ return ms + 'ms';
101
+ }
102
+ return (ms / 1000).toFixed(1) + 's';
103
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Reads a line with echo disabled (API keys). Falls back to a normal question
3
+ * when stdin is not a TTY.
4
+ */
5
+ export async function askHidden(rl, question) {
6
+ if (!process.stdin.isTTY) {
7
+ const answer = await rl.question(question);
8
+ return answer.trim();
9
+ }
10
+ rl.pause();
11
+ process.stdout.write(question);
12
+ const stdin = process.stdin;
13
+ stdin.setRawMode(true);
14
+ stdin.resume();
15
+ return await new Promise((resolve, reject) => {
16
+ let value = '';
17
+ const cleanup = () => {
18
+ stdin.setRawMode(false);
19
+ stdin.pause();
20
+ stdin.removeListener('data', onData);
21
+ rl.resume();
22
+ };
23
+ const onData = (chunk) => {
24
+ const text = chunk.toString('utf8');
25
+ for (const char of text) {
26
+ if (char === '\r' || char === '\n') {
27
+ cleanup();
28
+ process.stdout.write('\n');
29
+ resolve(value);
30
+ return;
31
+ }
32
+ if (char === '\u0003') {
33
+ cleanup();
34
+ process.stdout.write('\n');
35
+ reject(new Error('已取消'));
36
+ return;
37
+ }
38
+ if (char === '\u007f' || char === '\b') {
39
+ value = value.slice(0, -1);
40
+ continue;
41
+ }
42
+ if (char >= ' ') {
43
+ value += char;
44
+ }
45
+ }
46
+ };
47
+ stdin.on('data', onData);
48
+ });
49
+ }
@@ -0,0 +1,113 @@
1
+ import { visibleWidth } from "./ansi.js";
2
+ /** Editable single-line input with a char-level cursor and history. */
3
+ export class InputModel {
4
+ chars = [];
5
+ cursor = 0;
6
+ history = [];
7
+ historyIndex = -1;
8
+ draft = '';
9
+ get text() {
10
+ return this.chars.join('');
11
+ }
12
+ get empty() {
13
+ return this.chars.length === 0;
14
+ }
15
+ setText(text) {
16
+ this.chars = Array.from(text.replace(/\r\n?/g, '\n'));
17
+ this.cursor = this.chars.length;
18
+ }
19
+ insert(text) {
20
+ const incoming = Array.from(text.replace(/\r\n?/g, '\n'));
21
+ this.chars.splice(this.cursor, 0, ...incoming);
22
+ this.cursor += incoming.length;
23
+ }
24
+ backspace() {
25
+ if (this.cursor === 0) {
26
+ return;
27
+ }
28
+ this.chars.splice(this.cursor - 1, 1);
29
+ this.cursor -= 1;
30
+ }
31
+ deleteForward() {
32
+ if (this.cursor >= this.chars.length) {
33
+ return;
34
+ }
35
+ this.chars.splice(this.cursor, 1);
36
+ }
37
+ move(delta) {
38
+ this.cursor = Math.max(0, Math.min(this.chars.length, this.cursor + delta));
39
+ }
40
+ home() { this.cursor = 0; }
41
+ end() { this.cursor = this.chars.length; }
42
+ clear() {
43
+ this.chars = [];
44
+ this.cursor = 0;
45
+ this.historyIndex = -1;
46
+ }
47
+ remember() {
48
+ const value = this.text.trim();
49
+ if (value.length === 0) {
50
+ return;
51
+ }
52
+ if (this.history[this.history.length - 1] !== value) {
53
+ this.history.push(value);
54
+ }
55
+ this.historyIndex = -1;
56
+ this.draft = '';
57
+ }
58
+ previous() {
59
+ if (this.history.length === 0) {
60
+ return;
61
+ }
62
+ if (this.historyIndex === -1) {
63
+ this.draft = this.text;
64
+ this.historyIndex = this.history.length - 1;
65
+ }
66
+ else if (this.historyIndex > 0) {
67
+ this.historyIndex -= 1;
68
+ }
69
+ this.setText(this.history[this.historyIndex]);
70
+ }
71
+ next() {
72
+ if (this.historyIndex === -1) {
73
+ return;
74
+ }
75
+ if (this.historyIndex < this.history.length - 1) {
76
+ this.historyIndex += 1;
77
+ this.setText(this.history[this.historyIndex]);
78
+ return;
79
+ }
80
+ this.historyIndex = -1;
81
+ this.setText(this.draft);
82
+ }
83
+ }
84
+ /** Wraps the input text and reports where the caret sits inside it. */
85
+ export function wrapInput(text, caret, width) {
86
+ if (width < 4) {
87
+ return { lines: [text], row: 0, column: caret };
88
+ }
89
+ const chars = Array.from(text);
90
+ const lines = [''];
91
+ const widths = [0];
92
+ let caretRow = 0;
93
+ let caretColumn = 0;
94
+ for (let index = 0; index <= chars.length; index += 1) {
95
+ if (index === caret) {
96
+ caretRow = lines.length - 1;
97
+ caretColumn = widths[widths.length - 1];
98
+ }
99
+ if (index === chars.length) {
100
+ break;
101
+ }
102
+ const char = chars[index];
103
+ const charWidth = visibleWidth(char);
104
+ const last = lines.length - 1;
105
+ if (widths[last] + charWidth > width && lines[last].length > 0) {
106
+ lines.push('');
107
+ widths.push(0);
108
+ }
109
+ lines[lines.length - 1] += char;
110
+ widths[widths.length - 1] += charWidth;
111
+ }
112
+ return { lines, row: caretRow, column: caretColumn };
113
+ }
@@ -0,0 +1,142 @@
1
+ const SEQUENCES = [
2
+ ['\x1b[A', { type: 'up' }],
3
+ ['\x1b[B', { type: 'down' }],
4
+ ['\x1b[C', { type: 'right' }],
5
+ ['\x1b[D', { type: 'left' }],
6
+ ['\x1b[H', { type: 'home' }],
7
+ ['\x1b[F', { type: 'end' }],
8
+ ['\x1b[1~', { type: 'home' }],
9
+ ['\x1b[4~', { type: 'end' }],
10
+ ['\x1b[5~', { type: 'pageup' }],
11
+ ['\x1b[6~', { type: 'pagedown' }],
12
+ ['\x1b[3~', { type: 'delete' }],
13
+ ['\x1b[2~', { type: 'tab' }],
14
+ ['\x1bOA', { type: 'up' }],
15
+ ['\x1bOB', { type: 'down' }],
16
+ ['\x1bOC', { type: 'right' }],
17
+ ['\x1bOD', { type: 'left' }],
18
+ ];
19
+ const PASTE_START = '\x1b[200~';
20
+ const PASTE_END = '\x1b[201~';
21
+ /** Turns raw terminal chunks into key presses, tolerating split sequences. */
22
+ export class KeyParser {
23
+ buffer = '';
24
+ push(chunk) {
25
+ this.buffer += chunk;
26
+ const keys = [];
27
+ for (;;) {
28
+ if (this.buffer.length === 0) {
29
+ break;
30
+ }
31
+ if (this.buffer.startsWith(PASTE_START)) {
32
+ const end = this.buffer.indexOf(PASTE_END, PASTE_START.length);
33
+ if (end < 0) {
34
+ break;
35
+ }
36
+ keys.push({ type: 'paste', text: this.buffer.slice(PASTE_START.length, end) });
37
+ this.buffer = this.buffer.slice(end + PASTE_END.length);
38
+ continue;
39
+ }
40
+ if (this.buffer.startsWith('\x1b[<')) {
41
+ const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/.exec(this.buffer);
42
+ if (!match) {
43
+ break;
44
+ }
45
+ this.buffer = this.buffer.slice(match[0].length);
46
+ const button = Number(match[1]);
47
+ const row = Number(match[3]);
48
+ const col = Number(match[2]);
49
+ if (button === 64) {
50
+ keys.push({ type: 'wheel', delta: 3 });
51
+ }
52
+ else if (button === 65) {
53
+ keys.push({ type: 'wheel', delta: -3 });
54
+ }
55
+ else if (match[4] === 'm') {
56
+ keys.push({ type: 'mouse', kind: 'up', row, col });
57
+ }
58
+ else if (button === 0) {
59
+ keys.push({ type: 'mouse', kind: 'down', row, col });
60
+ }
61
+ else if (button === 32) {
62
+ keys.push({ type: 'mouse', kind: 'drag', row, col });
63
+ }
64
+ continue;
65
+ }
66
+ if (this.buffer.startsWith('\x1b[M')) {
67
+ if (this.buffer.length < 6) {
68
+ break;
69
+ }
70
+ this.buffer = this.buffer.slice(6);
71
+ continue;
72
+ }
73
+ let matched = false;
74
+ for (const [sequence, key] of SEQUENCES) {
75
+ if (this.buffer.startsWith(sequence)) {
76
+ keys.push(key);
77
+ this.buffer = this.buffer.slice(sequence.length);
78
+ matched = true;
79
+ break;
80
+ }
81
+ }
82
+ if (matched) {
83
+ continue;
84
+ }
85
+ if (this.buffer.startsWith('\x1b')) {
86
+ if (this.buffer.length < 3 && /^\x1b(\[<?|\[<|O)?$/.test(this.buffer)) {
87
+ break;
88
+ }
89
+ keys.push({ type: 'escape' });
90
+ this.buffer = this.buffer.slice(1);
91
+ continue;
92
+ }
93
+ const char = this.buffer[0];
94
+ const code = char.charCodeAt(0);
95
+ if (char === '\r' || char === '\n') {
96
+ keys.push({ type: 'enter' });
97
+ this.buffer = this.buffer.slice(1);
98
+ continue;
99
+ }
100
+ if (code === 0x7f || code === 0x08) {
101
+ keys.push({ type: 'backspace' });
102
+ this.buffer = this.buffer.slice(1);
103
+ continue;
104
+ }
105
+ if (code === 9) {
106
+ keys.push({ type: 'tab' });
107
+ this.buffer = this.buffer.slice(1);
108
+ continue;
109
+ }
110
+ if (code > 0 && code < 32) {
111
+ keys.push({ type: 'ctrl', key: String.fromCharCode(96 + code) });
112
+ this.buffer = this.buffer.slice(1);
113
+ continue;
114
+ }
115
+ let end = 0;
116
+ while (end < this.buffer.length) {
117
+ const value = this.buffer.charCodeAt(end);
118
+ if (value < 32 || value === 0x7f) {
119
+ break;
120
+ }
121
+ end += 1;
122
+ }
123
+ if (end === 0) {
124
+ break;
125
+ }
126
+ keys.push({ type: 'text', text: this.buffer.slice(0, end) });
127
+ this.buffer = this.buffer.slice(end);
128
+ }
129
+ return keys;
130
+ }
131
+ /** Called on a timer so a lone ESC arrives without waiting for more bytes. */
132
+ flush() {
133
+ if (this.buffer === '\x1b') {
134
+ this.buffer = '';
135
+ return [{ type: 'escape' }];
136
+ }
137
+ return [];
138
+ }
139
+ get pending() {
140
+ return this.buffer;
141
+ }
142
+ }
@@ -0,0 +1,34 @@
1
+ import { MASCOT_FRAMES, MASCOT_HEIGHT, MASCOT_WIDTH } from "./mascotData.js";
2
+ /** Half blocks pack two pixel rows into one terminal row. */
3
+ export const MASCOT_ROWS = Math.ceil(MASCOT_HEIGHT / 2);
4
+ export function renderMascot(grid, painter) {
5
+ const lines = [];
6
+ for (let row = 0; row < MASCOT_HEIGHT; row += 2) {
7
+ let line = '';
8
+ for (let col = 0; col < MASCOT_WIDTH; col += 1) {
9
+ const top = grid[row]?.[col] ?? null;
10
+ const bottom = grid[row + 1]?.[col] ?? null;
11
+ if (top !== null && bottom !== null) {
12
+ line += top === bottom
13
+ ? painter.fg(top, '█')
14
+ : painter.fg(top, painter.bg(bottom, '▀'));
15
+ }
16
+ else if (top !== null) {
17
+ line += painter.fg(top, '▀');
18
+ }
19
+ else if (bottom !== null) {
20
+ line += painter.fg(bottom, '▄');
21
+ }
22
+ else {
23
+ line += ' ';
24
+ }
25
+ }
26
+ lines.push(line);
27
+ }
28
+ return lines;
29
+ }
30
+ /** All animation frames rendered for the given painter. */
31
+ export function mascotFrames(painter) {
32
+ return MASCOT_FRAMES.map((grid) => renderMascot(grid, painter));
33
+ }
34
+ export const MASCOT_FRAME_NAMES = ['open', 'blink', 'open', 'bob', 'open'];