@hncode/hncode 0.1.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.
package/src/session.js ADDED
@@ -0,0 +1,73 @@
1
+ // Session store. Sessions persist under ~/.hncode/sessions/<id>.json.
2
+ // A session records: id, title, cwd (workspace), model, createdAt, updatedAt,
3
+ // and the message list (canonical, same shape the agent loop uses).
4
+
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import fs from 'node:fs';
8
+ import crypto from 'node:crypto';
9
+
10
+ const DEFAULT_DIR = () => path.join(os.homedir(), '.hncode', 'sessions');
11
+ function dir(custom) { return custom || process.env.HNCODE_SESSIONS_DIR || DEFAULT_DIR(); }
12
+
13
+ export function newId() {
14
+ // compact, mostly-sorted id (like a timestamp+rand ULID-ish)
15
+ return Date.now().toString(36) + '-' + crypto.randomBytes(3).toString('hex');
16
+ }
17
+
18
+ export function sessionFile(id, storeDir) {
19
+ return path.join(dir(storeDir), `${id}.json`);
20
+ }
21
+
22
+ export function saveSession(session, storeDir) {
23
+ session.updatedAt = Date.now();
24
+ fs.mkdirSync(dir(storeDir), { recursive: true });
25
+ const tmp = sessionFile(session.id, storeDir) + '.tmp';
26
+ fs.writeFileSync(tmp, JSON.stringify(session, null, 2), 'utf8');
27
+ fs.renameSync(tmp, sessionFile(session.id, storeDir));
28
+ return session;
29
+ }
30
+
31
+ export function loadSession(id, storeDir) {
32
+ try { return JSON.parse(fs.readFileSync(sessionFile(id, storeDir), 'utf8')); }
33
+ catch { return null; }
34
+ }
35
+
36
+ // Most recent session for a workspace, or globally.
37
+ // `opts.skipEmpty` ignores sessions with no messages: hncode creates an empty
38
+ // session shell on every launch, so without this `--continue` would "resume"
39
+ // a brand-new empty session instead of the user's last real conversation.
40
+ export function latestSession(cwd, storeDir, opts = {}) {
41
+ const d = dir(storeDir);
42
+ let files;
43
+ try { files = fs.readdirSync(d).filter((f) => f.endsWith('.json')); } catch { return null; }
44
+ let best = null;
45
+ let bestEmpty = null;
46
+ for (const f of files) {
47
+ try {
48
+ const s = JSON.parse(fs.readFileSync(path.join(d, f), 'utf8'));
49
+ if (cwd !== undefined && s.workspace !== path.resolve(cwd)) continue;
50
+ if (!best || s.updatedAt > best.updatedAt) best = s;
51
+ const hasMsgs = Array.isArray(s.messages) && s.messages.length > 0;
52
+ if (hasMsgs && (!bestEmpty || s.updatedAt > bestEmpty.updatedAt)) bestEmpty = s;
53
+ } catch {}
54
+ }
55
+ if (opts.skipEmpty) return bestEmpty || best;
56
+ return best;
57
+ }
58
+
59
+ export function listSessions(storeDir) {
60
+ const d = dir(storeDir);
61
+ let files;
62
+ try { files = fs.readdirSync(d).filter((f) => f.endsWith('.json')); } catch { return []; }
63
+ const out = [];
64
+ for (const f of files) {
65
+ try { out.push(JSON.parse(fs.readFileSync(path.join(d, f), 'utf8'))); } catch {}
66
+ }
67
+ out.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
68
+ return out;
69
+ }
70
+
71
+ export function deleteSession(id, storeDir) {
72
+ try { fs.unlinkSync(sessionFile(id, storeDir)); return true; } catch { return false; }
73
+ }
package/src/term.js ADDED
@@ -0,0 +1,292 @@
1
+ // Small TTY / terminal utilities.
2
+ // Test file for Write size display - this comment should be long enough to measure
3
+
4
+ export function isTTY() {
5
+ return !!process.stdout.isTTY;
6
+ }
7
+
8
+ export function size() {
9
+ try {
10
+ const [cols, rows] = process.stdout.getWindowSize();
11
+ return { cols: cols || 80, rows: rows || 24 };
12
+ } catch {
13
+ return { cols: 80, rows: 24 };
14
+ }
15
+ }
16
+
17
+ export function writeRaw(str) {
18
+ if (!process.stdout.write(str)) return false;
19
+ return true;
20
+ }
21
+
22
+ // Enable raw mode around a callback, restoring on exit/error.
23
+ export function withRawMode(cb) {
24
+ const stream = process.stdin;
25
+ if (!stream.isTTY) {
26
+ throw new Error('hncode: interactive mode requires a TTY (stdin is not a terminal).');
27
+ }
28
+ const wasRaw = stream.isRaw;
29
+ stream.setRawMode(true);
30
+ stream.resume();
31
+ stream.setEncoding('utf8');
32
+ const cleanup = () => {
33
+ try { stream.setRawMode(wasRaw || false); } catch {}
34
+ stream.pause();
35
+ };
36
+ try {
37
+ return cb(cleanup);
38
+ } finally {
39
+ cleanup();
40
+ }
41
+ }
42
+
43
+ // Visible width ignoring ANSI escapes (approx; no CJK width handling).
44
+ // NOTE: kept for back-compat; prefer visualWidth() for display-accurate width.
45
+ export function strWidth(s) {
46
+ return (s || '').replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').length;
47
+ }
48
+
49
+ // True visual column-width of a plain string (no ANSI), assuming a terminal that
50
+ // renders East Asian / wide / some symbol glyphs as 2 columns. Must be used for any
51
+ // cursor positioning and line wrapping so an I-shaped (insert) caret lines up.
52
+ // Source of truth: Unicode wide ranges + a small set of CJK-fused symbol glyphs.
53
+ export function visualWidth(s) {
54
+ if (!s) return 0;
55
+ // Measured per GRAPHEME CLUSTER (see clusterWidth): summing charWidth per
56
+ // codepoint made a ZWJ emoji sequence count 8 columns instead of 2.
57
+ return clusterWidth(s);
58
+ }
59
+
60
+ // ---- East Asian Width tables (Unicode 15) --------------------------------
61
+ // Wide/Fullwidth ranges -> 2 columns. Everything else is 1 unless it is a
62
+ // zero-width character (see ZERO_WIDTH) or emoji presentation (see below).
63
+ const WIDE = [
64
+ [0x1100, 0x115F], [0x231A, 0x231B], [0x2329, 0x232A], [0x23E9, 0x23EC],
65
+ [0x23F0, 0x23F0], [0x23F3, 0x23F3], [0x25FD, 0x25FE], [0x2614, 0x2615],
66
+ [0x2648, 0x2653], [0x267F, 0x267F], [0x2693, 0x2693], [0x26A1, 0x26A1],
67
+ [0x26AA, 0x26AB], [0x26BD, 0x26BE], [0x26C4, 0x26C5], [0x26CE, 0x26CE],
68
+ [0x26D4, 0x26D4], [0x26EA, 0x26EA], [0x26F2, 0x26F3], [0x26F5, 0x26F5],
69
+ [0x26FA, 0x26FA], [0x26FD, 0x26FD], [0x2705, 0x2705], [0x270A, 0x270B],
70
+ [0x2728, 0x2728], [0x274C, 0x274C], [0x274E, 0x274E], [0x2753, 0x2755],
71
+ [0x2757, 0x2757], [0x2795, 0x2797], [0x27B0, 0x27B0], [0x27BF, 0x27BF],
72
+ [0x2B1B, 0x2B1C], [0x2B50, 0x2B50], [0x2B55, 0x2B55],
73
+ [0x2E80, 0x303E], [0x3041, 0x33FF], [0x3400, 0x4DBF], [0x4E00, 0x9FFF],
74
+ [0xA000, 0xA4CF], [0xA960, 0xA97F], [0xAC00, 0xD7A3], [0xF900, 0xFAFF],
75
+ [0xFE10, 0xFE19], [0xFE30, 0xFE6F], [0xFF00, 0xFF60], [0xFFE0, 0xFFE6],
76
+ [0x16FE0, 0x16FE4], [0x17000, 0x187F7], [0x18800, 0x18CD5],
77
+ [0x1B000, 0x1B152], [0x1B164, 0x1B167], [0x1B170, 0x1B2FB],
78
+ [0x1F004, 0x1F004], [0x1F0CF, 0x1F0CF], [0x1F18E, 0x1F18E],
79
+ [0x1F191, 0x1F19A], [0x1F200, 0x1F320], [0x1F32D, 0x1F335],
80
+ [0x1F337, 0x1F37C], [0x1F37E, 0x1F393], [0x1F3A0, 0x1F3CA],
81
+ [0x1F3CF, 0x1F3D3], [0x1F3E0, 0x1F3F0], [0x1F3F4, 0x1F3F4],
82
+ [0x1F3F8, 0x1F43E], [0x1F440, 0x1F440], [0x1F442, 0x1F4FC],
83
+ [0x1F4FF, 0x1F53D], [0x1F54B, 0x1F54E], [0x1F550, 0x1F567],
84
+ [0x1F57A, 0x1F57A], [0x1F595, 0x1F596], [0x1F5A4, 0x1F5A4],
85
+ [0x1F5FB, 0x1F64F], [0x1F680, 0x1F6C5], [0x1F6CC, 0x1F6CC],
86
+ [0x1F6D0, 0x1F6D2], [0x1F6D5, 0x1F6D7], [0x1F6DC, 0x1F6DF],
87
+ [0x1F6EB, 0x1F6EC], [0x1F6F4, 0x1F6FC], [0x1F7E0, 0x1F7EB],
88
+ [0x1F7F0, 0x1F7F0], [0x1F90C, 0x1F93A], [0x1F93C, 0x1F945],
89
+ [0x1F947, 0x1F9FF], [0x1FA70, 0x1FAFF],
90
+ [0x20000, 0x2FFFD], [0x30000, 0x3FFFD],
91
+ ];
92
+
93
+ // Combining / formatting characters that occupy no cell.
94
+ function isZeroWidth(c) {
95
+ return (c >= 0x200B && c <= 0x200F) // ZWSP..RLM (incl. ZWJ U+200D)
96
+ || (c >= 0x0300 && c <= 0x036F)
97
+ || (c >= 0x0483 && c <= 0x0489)
98
+ || (c >= 0x0591 && c <= 0x05BD) || c === 0x05BF || (c >= 0x05C1 && c <= 0x05C2)
99
+ || (c >= 0x0610 && c <= 0x061A)
100
+ || (c >= 0x064B && c <= 0x065F) || c === 0x0670
101
+ || (c >= 0x06D6 && c <= 0x06DC)
102
+ || (c >= 0x0E31 && c <= 0x0E3A) || (c >= 0x0E47 && c <= 0x0E4E)
103
+ || (c >= 0x1AB0 && c <= 0x1AFF) || (c >= 0x1DC0 && c <= 0x1DFF)
104
+ || (c >= 0x20D0 && c <= 0x20FF) || (c >= 0xFE00 && c <= 0xFE0F)
105
+ || (c >= 0xFE20 && c <= 0xFE2F) || (c >= 0xE0100 && c <= 0xE01EF)
106
+ || (c >= 0x1160 && c <= 0x11FF); // Hangul Jamo medial/final
107
+ }
108
+
109
+ // Regional Indicator (flags) — two of them form one glyph.
110
+ const isRegional = (c) => c >= 0x1F1E6 && c <= 0x1F1FF;
111
+
112
+ // Emoji that occupy TWO cells by default (Emoji_Presentation=Yes), even
113
+ // without U+FE0F. Text-default symbols (U+276F ❯, U+2603 ☃, U+00B7 ·, …) are
114
+ // NOT in here: they are 1 cell unless followed by U+FE0F.
115
+ function isWideByDefault(c) {
116
+ return (c >= 0x1F300 && c <= 0x1F5FF)
117
+ || (c >= 0x1F600 && c <= 0x1F64F)
118
+ || (c >= 0x1F680 && c <= 0x1F6FF)
119
+ || (c >= 0x1F900 && c <= 0x1F9FF)
120
+ || (c >= 0x1FA70 && c <= 0x1FAFF)
121
+ || (c >= 0x1F000 && c <= 0x1F0FF)
122
+ || isRegional(c)
123
+ || c === 0x231A || c === 0x231B || c === 0x23E9 || c === 0x23EC
124
+ || c === 0x23F0 || c === 0x23F3 || c === 0x25FD || c === 0x25FE
125
+ || c === 0x2614 || c === 0x2615 || (c >= 0x2648 && c <= 0x2653)
126
+ || c === 0x267F || c === 0x2693 || c === 0x26A1 || (c >= 0x26AA && c <= 0x26AB)
127
+ || (c >= 0x26BD && c <= 0x26BE) || (c >= 0x26C4 && c <= 0x26C5)
128
+ || c === 0x26CE || c === 0x26D4 || c === 0x26EA
129
+ || (c >= 0x26F2 && c <= 0x26F3) || c === 0x26F5 || c === 0x26FA || c === 0x26FD
130
+ || c === 0x2705 || (c >= 0x270A && c <= 0x270B) || c === 0x2728
131
+ || c === 0x274C || c === 0x274E || (c >= 0x2753 && c <= 0x2755) || c === 0x2757
132
+ || (c >= 0x2795 && c <= 0x2797) || c === 0x27B0 || c === 0x27BF
133
+ || (c >= 0x2B1B && c <= 0x2B1C) || c === 0x2B50 || c === 0x2B55;
134
+ }
135
+
136
+ const inRanges = (c, ranges) => {
137
+ let lo = 0, hi = ranges.length - 1;
138
+ while (lo <= hi) {
139
+ const mid = (lo + hi) >> 1;
140
+ const [s, e] = ranges[mid];
141
+ if (c < s) hi = mid - 1;
142
+ else if (c > e) lo = mid + 1;
143
+ else return true;
144
+ }
145
+ return false;
146
+ };
147
+
148
+ export function charWidth(ch) {
149
+ const c = String(ch).codePointAt(0);
150
+ if (c === undefined) return 0;
151
+ // Tab renders as 4 spaces in most terminals.
152
+ if (c === 0x09) return 4;
153
+ if (c < 0x20 || c === 0x7F) return 0;
154
+ if (isZeroWidth(c)) return 0;
155
+ if (isWideByDefault(c)) return 2;
156
+ if (inRanges(c, WIDE)) return 2;
157
+ return 1;
158
+ }
159
+
160
+ // Split a string into grapheme clusters that a terminal renders as ONE glyph.
161
+ // Minimal but covers what actually appears in agent output: ZWJ sequences
162
+ // (👨‍👩‍👧), variation selectors (❤️), and flag pairs (🇺🇸).
163
+ function nextCluster(s, i) {
164
+ // NOTE: every offset here is a UTF-16 INDEX (what slice() consumes). Mixing
165
+ // in codepoint offsets desynced the scan across surrogate pairs, so a ZWJ
166
+ // family measured 5 columns instead of 2.
167
+ const at = (idx) => s.codePointAt(idx);
168
+ const len = (cp) => cp > 0xFFFF ? 2 : 1;
169
+
170
+ const firstCp = at(i);
171
+ let end = i + len(firstCp);
172
+ const startedRegional = isRegional(firstCp);
173
+
174
+ for (;;) {
175
+ if (end >= s.length) break;
176
+ const cp = at(end);
177
+ if (cp === 0x200D) { // ZWJ: absorb the next base too
178
+ end += 1;
179
+ if (end >= s.length) break;
180
+ end += len(at(end));
181
+ continue;
182
+ }
183
+ if (isZeroWidth(cp) || cp === 0xFE0F) { end += len(cp); continue; }
184
+ // A regional-indicator PAIR forms one flag glyph.
185
+ if (startedRegional && isRegional(cp)) { end += len(cp); break; }
186
+ break;
187
+ }
188
+ return { text: s.slice(i, end), next: end };
189
+ }
190
+
191
+ // Width of a whole string measured by GRAPHEME CLUSTER: a ZWJ sequence or a
192
+ // flag counts 2 columns total, not the sum of its codepoints (which used to
193
+ // make 👨‍👩‍👧 measure 8 columns and break every row it appeared on).
194
+ export function clusterWidth(s) {
195
+ const str = String(s || '');
196
+ let w = 0;
197
+ for (let i = 0; i < str.length;) {
198
+ const { text, next } = nextCluster(str, i);
199
+ i = next;
200
+ if (!text) { i++; continue; }
201
+ w += clusterGlyphWidth(text);
202
+ }
203
+ return w;
204
+ }
205
+
206
+ function clusterGlyphWidth(cluster) {
207
+ const cps = [...cluster].map((c) => c.codePointAt(0));
208
+ // ZWJ sequence / flag pair / VS16-presented symbol => 2 columns.
209
+ if (cps.includes(0x200D)) return 2;
210
+ if (cps.filter(isRegional).length >= 1) return 2;
211
+ const base = cps[0];
212
+ if (cps.includes(0xFE0F)) return 2; // explicit emoji presentation
213
+ const baseW = charWidth(String.fromCodePoint(base));
214
+ let extra = 0;
215
+ for (let k = 1; k < cps.length; k++) extra += charWidth(String.fromCodePoint(cps[k]));
216
+ return baseW + extra;
217
+ }
218
+
219
+ // Estimate the LLM token count of a plain string, matching kimi-code's
220
+ // algorithm in `llm-adapter/contract/tokens.ts`:
221
+ // tokens = ceil(ASCII chars / 4) + non-ASCII chars
222
+ // ASCII characters average ~4 per token; CJK and other non-ASCII are ~1 token
223
+ // each. This is far more accurate than a uniform chars/4 ratio.
224
+ export function estimateTokens(text) {
225
+ let ascii = 0, nonAscii = 0;
226
+ for (const ch of String(text ?? '')) {
227
+ if (ch.codePointAt(0) <= 127) ascii++;
228
+ else nonAscii++;
229
+ }
230
+ return Math.ceil(ascii / 4) + nonAscii;
231
+ }
232
+
233
+ // Estimate tokens for the whole request payload: system + tools + messages.
234
+ export function estimateMessagesTokens(messages, cfg) {
235
+ let total = estimateTokens(cfg && cfg.systemPrompt);
236
+ if (Array.isArray(cfg && cfg.toolFilter)) total += cfg.toolFilter.length * 8; // rough tool-def overhead
237
+ for (const m of messages || []) {
238
+ total += estimateTokens(m.role || '');
239
+ const c = m.content;
240
+ total += typeof c === 'string' ? estimateTokens(c) : estimateTokens(JSON.stringify(c || ''));
241
+ if (m.toolCalls) for (const tc of m.toolCalls) {
242
+ total += estimateTokens(tc.name || '') + estimateTokens(JSON.stringify(tc.args || ''));
243
+ }
244
+ }
245
+ return total;
246
+ }
247
+
248
+ // Expand TAB characters to spaces at 8-column tab stops. A terminal advances a
249
+ // tab to the NEXT tab stop, but our width math (visualWidth) counts '\t' as 0,
250
+ // so a row containing a tab is padded short and the terminal wraps it onto the
251
+ // next line — the "text bleeding onto another row" corruption. Tool output
252
+ // (e.g. `dir`, `ls -l`, git) contains tabs, so expand before measuring.
253
+ export function expandTabs(s, tabSize = 8) {
254
+ const str = String(s ?? '');
255
+ if (str.indexOf('\t') === -1) return str;
256
+ let out = '';
257
+ let col = 0;
258
+ for (const ch of str) {
259
+ if (ch === '\n') { out += ch; col = 0; continue; }
260
+ if (ch === '\t') {
261
+ const next = tabSize - (col % tabSize);
262
+ out += ' '.repeat(next);
263
+ col += next;
264
+ continue;
265
+ }
266
+ out += ch;
267
+ col += clusterWidth(ch);
268
+ }
269
+ return out;
270
+ }
271
+
272
+ // Truncate a string to `width` visible columns, stripping escapes for measurement
273
+ // and padding if shorter. Used for status/header lines. Honors 2-col glyphs.
274
+ export function fit(s, width) {
275
+ const plain = s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
276
+ const plainW = visualWidth(plain);
277
+ if (plainW <= width) return s + ' '.repeat(width - plainW);
278
+ let kept = '';
279
+ let w = 0;
280
+ for (const ch of s) {
281
+ if (ch === '\x1b') { kept += ch; continue; }
282
+ const cw = charWidth(ch);
283
+ if (w + cw > width) break;
284
+ w += cw;
285
+ kept += ch;
286
+ }
287
+ return kept;
288
+ }
289
+
290
+ export function emit(str) {
291
+ process.stdout.write(str);
292
+ }
package/src/toml.js ADDED
@@ -0,0 +1,281 @@
1
+ // Minimal but correct TOML parser (subset sufficient for hncode + ~/.kimi-code/config.toml).
2
+ // Supports: comments, basic & literal strings (incl. multiline), arrays (incl. multiline
3
+ // and trailing commas), inline tables, dotted keys, tables [a.b."c"], booleans, ints,
4
+ // floats, and dates (returned as strings). Arrays of tables [[...]] are NOT supported.
5
+
6
+ const T = (s) => new _T(s);
7
+
8
+ class _T {
9
+ constructor(s) { this.s = s; this.pos = 0; this.n = s.length; }
10
+ peek() { return this.pos < this.n ? this.s[this.pos] : ''; }
11
+ cur() { return this.pos < this.n ? this.s[this.pos] : ''; }
12
+ next() { return this.pos < this.n ? this.s[this.pos++] : ''; }
13
+ eof() { return this.pos >= this.n; }
14
+ copyUntil(re) {
15
+ let out = '';
16
+ while (!this.eof() && !re.test(this.cur())) out += this.next();
17
+ return out;
18
+ }
19
+ }
20
+
21
+ function isWhitespace(c) { return c === ' ' || c === '\t'; }
22
+ function isBareKey(c) { return /[A-Za-z0-9_-]/.test(c); }
23
+
24
+ export function parse(toml) {
25
+ if (toml.length === 0) return {};
26
+ const t = T(toml);
27
+ const root = {};
28
+ let current = root;
29
+ while (!t.eof()) {
30
+ skipTrivia(t);
31
+ if (t.eof()) break;
32
+ if (t.peek() === '[') {
33
+ // table header (or array-of-tables [[...]] which we reject)
34
+ t.next(); // consume '['
35
+ if (t.peek() === '[') {
36
+ t.next();
37
+ throw new Error('TOML: arrays of tables ([[...]]) are not supported');
38
+ }
39
+ const keys = parseKeyPath(t);
40
+ expect(t, ']');
41
+ t.next();
42
+ current = ensureTable(root, keys);
43
+ requireLineEnd(t);
44
+ } else {
45
+ // key = value
46
+ const keys = parseKeyPath(t);
47
+ skipTrivia(t);
48
+ expect(t, '=');
49
+ t.next();
50
+ skipTrivia(t);
51
+ const value = parseValue(t);
52
+ assign(current, keys, value);
53
+ requireLineEnd(t);
54
+ }
55
+ }
56
+ return root;
57
+ }
58
+
59
+ function skipTrivia(t) {
60
+ while (!t.eof()) {
61
+ const c = t.cur();
62
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { t.next(); continue; }
63
+ if (c === '#') {
64
+ // comment until end of line
65
+ while (!t.eof() && t.cur() !== '\n' && t.cur() !== '\r') t.next();
66
+ continue;
67
+ }
68
+ break;
69
+ }
70
+ }
71
+
72
+ function parseKeyPath(t) {
73
+ const parts = [];
74
+ do {
75
+ skipTrivia(t);
76
+ parts.push(parseKeyPart(t));
77
+ skipTrivia(t);
78
+ if (t.cur() === '.') { t.next(); continue; }
79
+ break;
80
+ } while (!t.eof());
81
+ if (parts.length === 0) throw new Error('TOML: expected key');
82
+ return parts;
83
+ }
84
+
85
+ function parseKeyPart(t) {
86
+ const c = t.cur();
87
+ if (c === '"') return parseBasicString(t, true);
88
+ if (c === "'") return parseLiteralString(t, true);
89
+ // bare key
90
+ let out = '';
91
+ while (!t.eof() && isBareKey(t.cur())) out += t.next();
92
+ if (!out) throw new Error(`TOML: invalid key char ${JSON.stringify(t.cur())}`);
93
+ return out;
94
+ }
95
+
96
+ function parseValue(t) {
97
+ skipTrivia(t);
98
+ const c = t.cur();
99
+ if (c === '"') return parseBasicString(t, false);
100
+ if (c === "'") return parseLiteralString(t, false);
101
+ if (c === '[') return parseArray(t);
102
+ if (c === '{') return parseInlineTable(t);
103
+ // scalar until delimiter
104
+ let raw = '';
105
+ while (!t.eof()) {
106
+ const cc = t.cur();
107
+ if (cc === '\n' || cc === '\r' || cc === ',' || cc === '}' || cc === ']' || cc === '#') break;
108
+ if (isWhitespace(cc)) break;
109
+ raw += t.next();
110
+ }
111
+ return parseScalar(raw);
112
+ }
113
+
114
+ function parseScalar(raw) {
115
+ if (raw === '') throw new Error('TOML: empty value');
116
+ const l = raw.toLowerCase();
117
+ if (l === 'true') return true;
118
+ if (l === 'false') return false;
119
+ // int
120
+ if (/^[+-]?\d+$/.test(raw)) return Number(raw);
121
+ // float (incl. exponents)
122
+ if (/^[+-]?(\d+\.\d*|\.\d+|\d+)([eE][+-]?\d+)?$/.test(raw) && !/^0[0-9]/.test(raw.replace(/^[+-]/,''))) {
123
+ const num = Number(raw);
124
+ if (!Number.isNaN(num)) return num;
125
+ }
126
+ // date/time (keep as string)
127
+ if (/^\d{4}-\d{2}-\d{2}([Tt]|\s)\d{2}:\d{2}:\d{2}/.test(raw) || /^\d{4}-\d{2}-\d{2}/.test(raw)) {
128
+ return raw;
129
+ }
130
+ return raw; // bare word fallback as string
131
+ }
132
+
133
+ function parseBasicString(t, isKey) {
134
+ t.next(); // opening "
135
+ if (t.peek() === '"' && t.next() === '"' && t.next() === '"') {
136
+ // multiline basic """
137
+ // line-ending-trim: skip a newline immediately following the opening """
138
+ if (t.cur() === '\n') t.next();
139
+ else if (t.cur() === '\r') { t.next(); if (t.cur() === '\n') t.next(); }
140
+ let out = '';
141
+ while (!t.eof()) {
142
+ const c = t.next();
143
+ if (c === '\\') {
144
+ const e = t.next();
145
+ out += unescapeBasic(e, t);
146
+ continue;
147
+ }
148
+ if (c === '"' && t.peek() === '"' && t.next() === '"' && t.next() === '"') break;
149
+ if (c === '\r' && t.cur() === '\n') { t.next(); out += '\n'; continue; }
150
+ out += c;
151
+ }
152
+ return out;
153
+ }
154
+ // single-line basic
155
+ let out = '';
156
+ while (!t.eof()) {
157
+ const c = t.next();
158
+ if (c === '"') { if (isKey) { break; } else { return out; } }
159
+ if (c === '\\') { const e = t.next(); out += unescapeBasic(e, t); continue; }
160
+ if (c === '\n' || c === '\r') throw new Error('TOML: unterminated basic string');
161
+ out += c;
162
+ }
163
+ return out;
164
+ }
165
+
166
+ function unescapeBasic(e, t) {
167
+ const m = {
168
+ b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\',
169
+ ' ' : ' ', '\t': '\t', '/': '/',
170
+ };
171
+ if (e in m) return m[e];
172
+ if (e === 'u') return readHex(t, 4);
173
+ if (e === 'U') return readHex(t, 8);
174
+ throw new Error(`TOML: invalid escape \\${e}`);
175
+ }
176
+
177
+ function readHex(t, count) {
178
+ let hex = '';
179
+ for (let i = 0; i < count; i++) hex += t.next();
180
+ return String.fromCodePoint(parseInt(hex, 16));
181
+ }
182
+
183
+ function parseLiteralString(t, isKey) {
184
+ t.next(); // opening '
185
+ if (t.peek() === "'" && t.next() === "'" && t.next() === "'") {
186
+ // multiline literal '''
187
+ if (t.cur() === '\n') t.next();
188
+ else if (t.cur() === '\r') { t.next(); if (t.cur() === '\n') t.next(); }
189
+ let out = '';
190
+ while (!t.eof()) {
191
+ const c = t.next();
192
+ if (c === "'" && t.peek() === "'" && t.next() === "'" && t.next() === "'") break;
193
+ out += c;
194
+ }
195
+ return out;
196
+ }
197
+ let out = '';
198
+ while (!t.eof()) {
199
+ const c = t.next();
200
+ if (c === "'") break;
201
+ if (c === '\n' || c === '\r') throw new Error('TOML: unterminated literal string');
202
+ out += c;
203
+ }
204
+ return out;
205
+ }
206
+
207
+ function parseArray(t) {
208
+ t.next(); // [
209
+ const arr = [];
210
+ while (true) {
211
+ skipTrivia(t);
212
+ if (t.cur() === ']') { t.next(); break; }
213
+ if (t.eof()) throw new Error('TOML: unterminated array');
214
+ arr.push(parseValue(t));
215
+ skipTrivia(t);
216
+ if (t.cur() === ',') { t.next(); continue; }
217
+ if (t.cur() === ']') { t.next(); break; }
218
+ if (t.eof()) throw new Error('TOML: unterminated array');
219
+ throw new Error(`TOML: expected ',' or ']' in array, got ${JSON.stringify(t.cur())}`);
220
+ }
221
+ return arr;
222
+ }
223
+
224
+ function parseInlineTable(t) {
225
+ t.next(); // {
226
+ const obj = {};
227
+ while (true) {
228
+ skipTrivia(t);
229
+ if (t.cur() === '}') { t.next(); break; }
230
+ if (t.eof()) throw new Error('TOML: unterminated inline table');
231
+ const keys = parseKeyPath(t);
232
+ skipTrivia(t);
233
+ expect(t, '=');
234
+ t.next();
235
+ const value = parseValue(t);
236
+ assign(obj, keys, value);
237
+ skipTrivia(t);
238
+ if (t.cur() === ',') { t.next(); continue; }
239
+ if (t.cur() === '}') { t.next(); break; }
240
+ }
241
+ return obj;
242
+ }
243
+
244
+ function expect(t, ch) {
245
+ if (t.cur() !== ch) throw new Error(`TOML: expected '${ch}' got ${JSON.stringify(t.cur())} at ${t.pos}`);
246
+ }
247
+
248
+ function requireLineEnd(t) {
249
+ // After a key=value or table header: only skip trailing spaces/tabs on this
250
+ // line (and an optional trailing comment), then require a line boundary.
251
+ // We do NOT consume the newline here; the main loop's skipTrivia() will.
252
+ while (!t.eof() && (t.cur() === ' ' || t.cur() === '\t')) t.next();
253
+ if (t.cur() === '#') {
254
+ while (!t.eof() && t.cur() !== '\n' && t.cur() !== '\r') t.next();
255
+ }
256
+ if (!t.eof() && t.cur() !== '\n' && t.cur() !== '\r') {
257
+ throw new Error(`TOML: expected end of line at ${t.pos}, got ${JSON.stringify(t.cur())}`);
258
+ }
259
+ }
260
+
261
+ function ensureTable(root, keys) {
262
+ let o = root;
263
+ for (const k of keys) {
264
+ if (!(k in o) || typeof o[k] !== 'object' || Array.isArray(o[k])) o[k] = {};
265
+ o = o[k];
266
+ }
267
+ return o;
268
+ }
269
+
270
+ function assign(table, keys, value) {
271
+ let o = table;
272
+ for (let i = 0; i < keys.length - 1; i++) {
273
+ const k = keys[i];
274
+ if (!(k in o) || typeof o[k] !== 'object' || Array.isArray(o[k])) o[k] = {};
275
+ o = o[k];
276
+ }
277
+ const last = keys[keys.length - 1];
278
+ o[last] = value;
279
+ }
280
+
281
+ export default { parse };