novac 2.1.1 → 2.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 (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +0 -0
  3. package/demo.nv +0 -0
  4. package/demo_builtins.nv +0 -0
  5. package/demo_http.nv +0 -0
  6. package/examples/bf.nv +69 -0
  7. package/examples/math.nv +21 -0
  8. package/kits/birdAPI/kitdef.js +954 -0
  9. package/kits/kitRNG/kitdef.js +740 -0
  10. package/kits/kitSSH/kitdef.js +1272 -0
  11. package/kits/kitadb/kitdef.js +606 -0
  12. package/kits/kitai/kitdef.js +2185 -0
  13. package/kits/kitcanvas/kitdef.js +914 -0
  14. package/kits/kitclippy/kitdef.js +925 -0
  15. package/kits/kitgps/kitdef.js +1862 -0
  16. package/kits/kitlibproc/kitdef.js +3 -2
  17. package/kits/kitmorse/kitdef.js +229 -0
  18. package/kits/kitmpatch/kitdef.js +906 -0
  19. package/kits/kitnet/kitdef.js +1401 -0
  20. package/kits/kitproto/kitdef.js +613 -0
  21. package/kits/kitqr/kitdef.js +637 -0
  22. package/kits/kitrequire/kitdef.js +1599 -0
  23. package/kits/libtea/kitdef.js +2691 -0
  24. package/kits/libterm/kitdef.js +2 -0
  25. package/novac/LICENSE +21 -0
  26. package/novac/README.md +1823 -0
  27. package/novac/bin/novac +950 -0
  28. package/novac/bin/nvc +522 -0
  29. package/novac/bin/nvml +542 -0
  30. package/novac/demo.nv +245 -0
  31. package/novac/demo_builtins.nv +209 -0
  32. package/novac/demo_http.nv +62 -0
  33. package/novac/examples/bf.nv +69 -0
  34. package/novac/examples/math.nv +21 -0
  35. package/novac/kits/kitai/kitdef.js +2185 -0
  36. package/novac/kits/kitansi/kitdef.js +1402 -0
  37. package/novac/kits/kitformat/kitdef.js +1485 -0
  38. package/novac/kits/kitgps/kitdef.js +1862 -0
  39. package/novac/kits/kitlibfs/kitdef.js +231 -0
  40. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  41. package/novac/kits/kitmatrix/ex.js +19 -0
  42. package/novac/kits/kitmatrix/kitdef.js +960 -0
  43. package/novac/kits/kitmpatch/kitdef.js +906 -0
  44. package/novac/kits/kitnovacweb/README.md +1572 -0
  45. package/novac/kits/kitnovacweb/demo.nv +12 -0
  46. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  47. package/novac/kits/kitnovacweb/index.nova +12 -0
  48. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  49. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  50. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  51. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  52. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  53. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  54. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  55. package/novac/kits/kitparse/kitdef.js +1688 -0
  56. package/novac/kits/kitregex++/kitdef.js +1353 -0
  57. package/novac/kits/kitrequire/kitdef.js +1599 -0
  58. package/novac/kits/kitx11/kitdef.js +1 -0
  59. package/novac/kits/kitx11/kitx11.js +2472 -0
  60. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  61. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  62. package/novac/kits/libterm/ex.js +285 -0
  63. package/novac/kits/libterm/kitdef.js +1927 -0
  64. package/novac/node_modules/chalk/license +9 -0
  65. package/novac/node_modules/chalk/package.json +83 -0
  66. package/novac/node_modules/chalk/readme.md +297 -0
  67. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  68. package/novac/node_modules/chalk/source/index.js +225 -0
  69. package/novac/node_modules/chalk/source/utilities.js +33 -0
  70. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  71. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  72. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  73. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  74. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  75. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  76. package/novac/node_modules/commander/LICENSE +22 -0
  77. package/novac/node_modules/commander/Readme.md +1176 -0
  78. package/novac/node_modules/commander/esm.mjs +16 -0
  79. package/novac/node_modules/commander/index.js +24 -0
  80. package/novac/node_modules/commander/lib/argument.js +150 -0
  81. package/novac/node_modules/commander/lib/command.js +2777 -0
  82. package/novac/node_modules/commander/lib/error.js +39 -0
  83. package/novac/node_modules/commander/lib/help.js +747 -0
  84. package/novac/node_modules/commander/lib/option.js +380 -0
  85. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  86. package/novac/node_modules/commander/package-support.json +19 -0
  87. package/novac/node_modules/commander/package.json +82 -0
  88. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  89. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  90. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  91. package/novac/node_modules/node-addon-api/README.md +95 -0
  92. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  93. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  94. package/novac/node_modules/node-addon-api/index.js +14 -0
  95. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  96. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  97. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  98. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  99. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  100. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  101. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  102. package/novac/node_modules/node-addon-api/package.json +480 -0
  103. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  104. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  105. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  106. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  107. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  108. package/novac/node_modules/serialize-javascript/README.md +149 -0
  109. package/novac/node_modules/serialize-javascript/index.js +297 -0
  110. package/novac/node_modules/serialize-javascript/package.json +33 -0
  111. package/novac/package.json +27 -0
  112. package/novac/scripts/update-bin.js +24 -0
  113. package/novac/src/core/bstd.js +1035 -0
  114. package/novac/src/core/config.js +155 -0
  115. package/novac/src/core/describe.js +187 -0
  116. package/novac/src/core/emitter.js +499 -0
  117. package/novac/src/core/error.js +86 -0
  118. package/novac/src/core/executor.js +5606 -0
  119. package/novac/src/core/formatter.js +686 -0
  120. package/novac/src/core/lexer.js +1026 -0
  121. package/novac/src/core/nova_builtins.js +717 -0
  122. package/novac/src/core/nova_thread_worker.js +166 -0
  123. package/novac/src/core/parser.js +2181 -0
  124. package/novac/src/core/types.js +112 -0
  125. package/novac/src/index.js +28 -0
  126. package/novac/src/runtime/stdlib.js +244 -0
  127. package/package.json +3 -2
  128. package/scripts/update-bin.js +0 -0
  129. package/src/core/bstd.js +835 -361
  130. package/src/core/executor.js +427 -246
  131. package/src/core/lexer.js +19 -2
  132. package/src/core/parser.js +13 -0
  133. package/src/index.js +0 -0
  134. package/examples/example-project/README.md +0 -3
  135. package/examples/example-project/src/main.nova +0 -3
  136. package/src/core/environment.js +0 -0
  137. /package/{kits → novac/kits}/libtea/tf.js +0 -0
  138. /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
@@ -0,0 +1,925 @@
1
+ // kitdef.js — kitClipboard
2
+ // Cross-platform clipboard management: read, write, file copy, format detection,
3
+ // history, monitoring, paste detection, and more.
4
+ //
5
+ // Platform support: Windows, macOS, Linux (X11/Wayland), Android (Termux), iOS
6
+ // Backends: pbcopy/pbpaste (macOS), xclip/xsel/wl-clipboard (Linux),
7
+ // clip/powershell (Windows), Termux API (Android)
8
+ //
9
+ // const { kitClipboard } = require('./kitdef').kitdef;
10
+
11
+ const { execSync, execFileSync, spawn } = require("child_process");
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const os = require("os");
15
+
16
+ // ── Platform detection ───────────────────────────────────────────────────────
17
+
18
+ function _platform() {
19
+ const p = process.platform;
20
+ if (p === "darwin") return "macos";
21
+ if (p === "win32") return "windows";
22
+ if (p === "android" || process.env.TERMUX_VERSION) return "android";
23
+ // Linux: detect Wayland vs X11
24
+ if (process.env.WAYLAND_DISPLAY) return "wayland";
25
+ if (process.env.DISPLAY) return "x11";
26
+ return "linux"; // fallback
27
+ }
28
+
29
+ function _hasCmd(cmd) {
30
+ try { execSync(`command -v ${cmd}`, { stdio: "pipe" }); return true; }
31
+ catch { return false; }
32
+ }
33
+
34
+ function _linuxBackend() {
35
+ if (_platform() === "wayland" && _hasCmd("wl-copy")) return "wayland";
36
+ if (_hasCmd("xclip")) return "xclip";
37
+ if (_hasCmd("xsel")) return "xsel";
38
+ if (_hasCmd("wl-copy")) return "wayland";
39
+ throw new Error("No clipboard backend found. Install xclip, xsel, or wl-clipboard.");
40
+ }
41
+
42
+ // ── Bracketed Paste Mode (terminal escape sequence) ──────────────────────────
43
+ //
44
+ // When a terminal supports bracketed paste mode (\x1b[?2004h), pasted text is
45
+ // wrapped between \x1b[200~ ... \x1b[201~, making it unambiguously distinct
46
+ // from keyboard input. We enable it on stdin when it's a TTY, listen for the
47
+ // markers, and set a flag that detectSource() can read.
48
+ //
49
+ // References:
50
+ // https://cirw.in/blog/bracketed-paste
51
+ // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html (DEC mode 2004)
52
+
53
+ const PASTE_START = "\x1b[200~";
54
+ const PASTE_END = "\x1b[201~";
55
+ const BP_ENABLE = "\x1b[?2004h";
56
+ const BP_DISABLE = "\x1b[?2004l";
57
+
58
+ let _bracketedPasteSupported = false;
59
+ let _lastInputSource = "unknown"; // "paste" | "typed" | "unknown"
60
+ let _bpBuffer = ""; // accumulates raw stdin bytes
61
+
62
+ function _enableBracketedPaste() {
63
+ if (!process.stdin.isTTY) return;
64
+ try {
65
+ process.stdout.write(BP_ENABLE);
66
+ _bracketedPasteSupported = true;
67
+
68
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
69
+ process.stdin.resume();
70
+ process.stdin.setEncoding("utf8");
71
+
72
+ process.stdin.on("data", (chunk) => {
73
+ _bpBuffer += chunk;
74
+
75
+ // Consume all complete bracketed-paste sequences
76
+ let start;
77
+ while ((start = _bpBuffer.indexOf(PASTE_START)) !== -1) {
78
+ const end = _bpBuffer.indexOf(PASTE_END, start + PASTE_START.length);
79
+ if (end === -1) break; // incomplete sequence — wait for more data
80
+ // Everything between the markers is pasted text
81
+ _lastInputSource = "paste";
82
+ _bpBuffer = _bpBuffer.slice(0, start) + _bpBuffer.slice(end + PASTE_END.length);
83
+ }
84
+
85
+ // Anything left in the buffer (outside markers) is typed
86
+ if (_bpBuffer.length > 0 && !_bpBuffer.includes(PASTE_START)) {
87
+ _lastInputSource = "typed";
88
+ _bpBuffer = "";
89
+ }
90
+ });
91
+
92
+ // Restore terminal on exit
93
+ const _restore = () => {
94
+ try { process.stdout.write(BP_DISABLE); } catch {}
95
+ };
96
+ process.once("exit", _restore);
97
+ process.once("SIGINT", () => { _restore(); process.exit(130); });
98
+ process.once("SIGTERM", () => { _restore(); process.exit(143); });
99
+ } catch {
100
+ // Non-TTY or terminal doesn't support bracketed paste — fall through to
101
+ // the content-fingerprint fallback in detectSource().
102
+ _bracketedPasteSupported = false;
103
+ }
104
+ }
105
+
106
+ // Attempt to enable bracketed paste immediately.
107
+ _enableBracketedPaste();
108
+
109
+ // ── Internal state ───────────────────────────────────────────────────────────
110
+
111
+ const _history = []; // { text, timestamp, source }
112
+ const _MAX_HIST = 100;
113
+ let _lastWrite = null; // timestamp of last kitClipboard.write() call
114
+ let _lastWriteContent = null; // exact string written — used by content-fingerprint fallback
115
+ let _watchTimer = null;
116
+ let _watchLast = null;
117
+
118
+ function _pushHistory(text, source = "external") {
119
+ _history.unshift({ text, timestamp: Date.now(), source });
120
+ if (_history.length > _MAX_HIST) _history.pop();
121
+ }
122
+
123
+ // ── Low-level read/write ─────────────────────────────────────────────────────
124
+
125
+ function _read() {
126
+ const plat = _platform();
127
+ switch (plat) {
128
+ case "macos":
129
+ return execSync("pbpaste", { encoding: "utf8" });
130
+
131
+ case "windows":
132
+ // PowerShell gives us Unicode-safe clipboard access
133
+ return execSync(
134
+ `powershell -NoProfile -NonInteractive -Command "Get-Clipboard"`,
135
+ { encoding: "utf8" }
136
+ ).replace(/\r\n/g, "\n");
137
+
138
+ case "android":
139
+ return execSync("termux-clipboard-get", { encoding: "utf8" });
140
+
141
+ case "wayland": {
142
+ const backend = _linuxBackend();
143
+ if (backend === "wayland") return execSync("wl-paste --no-newline", { encoding: "utf8" });
144
+ // fallthrough to x11 backends
145
+ }
146
+ // eslint-disable-next-line no-fallthrough
147
+ case "x11":
148
+ case "linux": {
149
+ const backend = _linuxBackend();
150
+ if (backend === "xclip") return execSync("xclip -selection clipboard -o", { encoding: "utf8" });
151
+ if (backend === "xsel") return execSync("xsel --clipboard --output", { encoding: "utf8" });
152
+ if (backend === "wayland") return execSync("wl-paste --no-newline", { encoding: "utf8" });
153
+ throw new Error("No clipboard backend available.");
154
+ }
155
+
156
+ default:
157
+ throw new Error(`Unsupported platform: ${plat}`);
158
+ }
159
+ }
160
+
161
+ function _write(text) {
162
+ const plat = _platform();
163
+ switch (plat) {
164
+ case "macos":
165
+ execSync("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
166
+ break;
167
+
168
+ case "windows":
169
+ // Use clip.exe for simple text; PowerShell for Unicode safety
170
+ execSync(
171
+ `powershell -NoProfile -NonInteractive -Command "Set-Clipboard -Value @'\n${text.replace(/'/g, "''")}\n'@"`,
172
+ { stdio: "pipe" }
173
+ );
174
+ break;
175
+
176
+ case "android":
177
+ execSync(`termux-clipboard-set`, { input: text, stdio: ["pipe", "ignore", "ignore"] });
178
+ break;
179
+
180
+ case "wayland":
181
+ case "x11":
182
+ case "linux": {
183
+ const backend = _linuxBackend();
184
+ if (backend === "xclip")
185
+ execSync("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
186
+ else if (backend === "xsel")
187
+ execSync("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
188
+ else if (backend === "wayland")
189
+ execSync("wl-copy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
190
+ break;
191
+ }
192
+
193
+ default:
194
+ throw new Error(`Unsupported platform: ${plat}`);
195
+ }
196
+ }
197
+
198
+ // ── Export ───────────────────────────────────────────────────────────────────
199
+
200
+ module.exports = {
201
+ kitdef: {
202
+
203
+ name: "kitClipboard",
204
+ version: "1.0.0",
205
+ description: "Cross-platform clipboard management: read, write, file copy, history, monitoring, format detection, and paste detection.",
206
+
207
+ // ──────────────────────────────────────────────────────────────────────
208
+ // READ
209
+ // ──────────────────────────────────────────────────────────────────────
210
+
211
+ /**
212
+ * Read the current clipboard contents as a string.
213
+ * @returns {string}
214
+ *
215
+ * @example
216
+ * const text = kitClipboard.read();
217
+ */
218
+ read() {
219
+ return _read();
220
+ },
221
+
222
+ /**
223
+ * Read clipboard and return trimmed text.
224
+ * @returns {string}
225
+ */
226
+ readTrimmed() {
227
+ return _read().trim();
228
+ },
229
+
230
+ /**
231
+ * Read clipboard and parse as JSON.
232
+ * Throws if content is not valid JSON.
233
+ * @returns {any}
234
+ */
235
+ readJSON() {
236
+ const text = _read().trim();
237
+ try { return JSON.parse(text); }
238
+ catch { throw new Error("Clipboard content is not valid JSON."); }
239
+ },
240
+
241
+ /**
242
+ * Read clipboard and split into lines.
243
+ * @param {{ keepEmpty?: boolean }} [options]
244
+ * @returns {string[]}
245
+ */
246
+ readLines(options = {}) {
247
+ const lines = _read().split("\n");
248
+ return options.keepEmpty ? lines : lines.filter(l => l.trim().length > 0);
249
+ },
250
+
251
+ /**
252
+ * Read clipboard and return as a Buffer.
253
+ * @returns {Buffer}
254
+ */
255
+ readBuffer() {
256
+ return Buffer.from(_read(), "utf8");
257
+ },
258
+
259
+ /**
260
+ * Read clipboard and return as a base64 string.
261
+ * @returns {string}
262
+ */
263
+ readBase64() {
264
+ return Buffer.from(_read(), "utf8").toString("base64");
265
+ },
266
+
267
+ // ──────────────────────────────────────────────────────────────────────
268
+ // WRITE
269
+ // ──────────────────────────────────────────────────────────────────────
270
+
271
+ /**
272
+ * Write text to the clipboard.
273
+ * @param {string} text
274
+ * @returns {{ written: string, length: number }}
275
+ *
276
+ * @example
277
+ * kitClipboard.write("Hello, world!");
278
+ */
279
+ write(text) {
280
+ const str = String(text);
281
+ _lastWrite = Date.now();
282
+ _lastWriteContent = str; // fingerprint fallback: remember exact content
283
+ _lastInputSource = "unknown"; // clear TTY source — clipboard is now ours
284
+ _write(str);
285
+ _pushHistory(str, "write");
286
+ return { written: str, length: str.length };
287
+ },
288
+
289
+ /**
290
+ * Write JSON to the clipboard (pretty-printed).
291
+ * @param {any} value
292
+ * @param {number} [indent=2]
293
+ * @returns {{ written: string }}
294
+ */
295
+ writeJSON(value, indent = 2) {
296
+ return this.write(JSON.stringify(value, null, indent));
297
+ },
298
+
299
+ /**
300
+ * Append text to existing clipboard content.
301
+ * @param {string} text
302
+ * @param {string} [separator="\n"]
303
+ * @returns {{ written: string }}
304
+ */
305
+ append(text, separator = "\n") {
306
+ const current = _read();
307
+ return this.write(current + separator + text);
308
+ },
309
+
310
+ /**
311
+ * Prepend text to existing clipboard content.
312
+ * @param {string} text
313
+ * @param {string} [separator="\n"]
314
+ * @returns {{ written: string }}
315
+ */
316
+ prepend(text, separator = "\n") {
317
+ const current = _read();
318
+ return this.write(text + separator + current);
319
+ },
320
+
321
+ /**
322
+ * Clear the clipboard.
323
+ * @returns {{ status: string }}
324
+ */
325
+ clear() {
326
+ _write("");
327
+ _lastWrite = Date.now();
328
+ return { status: "CLEARED" };
329
+ },
330
+
331
+ /**
332
+ * Write from a Buffer or base64 string.
333
+ * @param {Buffer|string} data
334
+ * @returns {{ written: number }}
335
+ */
336
+ writeBuffer(data) {
337
+ const str = Buffer.isBuffer(data)
338
+ ? data.toString("utf8")
339
+ : Buffer.from(data, "base64").toString("utf8");
340
+ _write(str);
341
+ _lastWrite = Date.now();
342
+ _pushHistory(str, "writeBuffer");
343
+ return { written: str.length };
344
+ },
345
+
346
+ // ──────────────────────────────────────────────────────────────────────
347
+ // FILE OPERATIONS
348
+ // ──────────────────────────────────────────────────────────────────────
349
+
350
+ /**
351
+ * Copy the contents of a file to the clipboard.
352
+ * @param {string} filePath
353
+ * @param {BufferEncoding} [encoding="utf8"]
354
+ * @returns {{ file: string, length: number }}
355
+ *
356
+ * @example
357
+ * kitClipboard.copyFile("./README.md");
358
+ */
359
+ copyFile(filePath, encoding = "utf8") {
360
+ const resolved = filePath.replace(/^~/, os.homedir());
361
+ const content = fs.readFileSync(resolved, encoding);
362
+ this.write(content);
363
+ return { file: resolved, length: content.length };
364
+ },
365
+
366
+ /**
367
+ * Save the current clipboard content to a file.
368
+ * @param {string} filePath
369
+ * @param {BufferEncoding} [encoding="utf8"]
370
+ * @returns {{ file: string, length: number }}
371
+ *
372
+ * @example
373
+ * kitClipboard.saveToFile("./output.txt");
374
+ */
375
+ saveToFile(filePath, encoding = "utf8") {
376
+ const resolved = filePath.replace(/^~/, os.homedir());
377
+ const content = _read();
378
+ fs.writeFileSync(resolved, content, encoding);
379
+ return { file: resolved, length: content.length };
380
+ },
381
+
382
+ /**
383
+ * Append the current clipboard content to a file.
384
+ * @param {string} filePath
385
+ * @returns {{ file: string, appended: number }}
386
+ */
387
+ appendToFile(filePath) {
388
+ const resolved = filePath.replace(/^~/, os.homedir());
389
+ const content = _read();
390
+ fs.appendFileSync(resolved, content, "utf8");
391
+ return { file: resolved, appended: content.length };
392
+ },
393
+
394
+ /**
395
+ * Copy multiple files' contents to clipboard, joined by a separator.
396
+ * @param {string[]} filePaths
397
+ * @param {string} [separator="\n---\n"]
398
+ * @returns {{ files: string[], totalLength: number }}
399
+ */
400
+ copyFiles(filePaths, separator = "\n---\n") {
401
+ const contents = filePaths.map(f => {
402
+ const resolved = f.replace(/^~/, os.homedir());
403
+ return `// ${path.basename(resolved)}\n` + fs.readFileSync(resolved, "utf8");
404
+ });
405
+ const joined = contents.join(separator);
406
+ this.write(joined);
407
+ return { files: filePaths, totalLength: joined.length };
408
+ },
409
+
410
+ // ──────────────────────────────────────────────────────────────────────
411
+ // PASTE DETECTION
412
+ // ──────────────────────────────────────────────────────────────────────
413
+
414
+ /**
415
+ * Detect whether the current clipboard content was written programmatically
416
+ * (via kitClipboard.write()) or pasted/typed by a human.
417
+ *
418
+ * Three-tier detection strategy (most → least reliable):
419
+ *
420
+ * TIER 1 — Terminal bracketed paste mode (exact, ~100%)
421
+ * If the process is running in a TTY that supports DEC mode 2004
422
+ * (\x1b[?2004h), pasted text arrives wrapped between \x1b[200~ and
423
+ * \x1b[201~. The stdin listener set up by _enableBracketedPaste()
424
+ * records whether the last input event was a paste or a keypress and
425
+ * stores it in _lastInputSource. When this information is available
426
+ * we trust it unconditionally — it comes straight from the terminal.
427
+ *
428
+ * TIER 2 — Content fingerprint (exact, ~100%)
429
+ * Every kitClipboard.write() call stores the exact string it wrote in
430
+ * _lastWriteContent. We read the clipboard right now and compare. If
431
+ * the live content === _lastWriteContent we know it is still our write.
432
+ * If it differs, something external changed it (human paste, another
433
+ * program). This is purely observational — no timing, no races.
434
+ * It fails only in the astronomically unlikely event that a human
435
+ * happens to paste the exact same string we last wrote (still detected
436
+ * as "programmatic" — an acceptable false-negative).
437
+ *
438
+ * TIER 3 — Timing heuristic (fallback, ~95%)
439
+ * If we have never written anything (_lastWrite === null) we return
440
+ * "unknown". Otherwise we use the age of the last write: if the last
441
+ * write happened very recently (< 200 ms) we call it "programmatic";
442
+ * otherwise "typed". This is the weakest tier and is only reached
443
+ * when _lastWriteContent is unavailable (e.g. after a process restart
444
+ * that re-uses an existing clipboard value).
445
+ *
446
+ * @returns {{
447
+ * source: 'programmatic'|'typed'|'unknown',
448
+ * method: 'bracketed-paste'|'content-fingerprint'|'timing-heuristic'|'no-data',
449
+ * lastWriteMs: number|null,
450
+ * ageMs: number|null,
451
+ * }}
452
+ */
453
+ detectSource() {
454
+ // ── Tier 1: bracketed paste ──────────────────────────────────────────
455
+ if (_bracketedPasteSupported && _lastInputSource !== "unknown") {
456
+ return {
457
+ source: _lastInputSource === "paste" ? "typed" : "programmatic",
458
+ method: "bracketed-paste",
459
+ lastWriteMs: _lastWrite,
460
+ ageMs: _lastWrite !== null ? Date.now() - _lastWrite : null,
461
+ };
462
+ }
463
+
464
+ // ── Tier 2: content fingerprint ─────────────────────────────────────
465
+ // Read the live clipboard and compare to what we last wrote.
466
+ if (_lastWriteContent !== null) {
467
+ let liveContent;
468
+ try { liveContent = _read(); } catch { liveContent = null; }
469
+
470
+ if (liveContent !== null) {
471
+ const isOurs = liveContent === _lastWriteContent;
472
+ return {
473
+ source: isOurs ? "programmatic" : "typed",
474
+ method: "content-fingerprint",
475
+ lastWriteMs: _lastWrite,
476
+ ageMs: _lastWrite !== null ? Date.now() - _lastWrite : null,
477
+ };
478
+ }
479
+ }
480
+
481
+ // ── Tier 3: timing heuristic (last resort) ───────────────────────────
482
+ if (_lastWrite === null) {
483
+ return { source: "unknown", method: "no-data", lastWriteMs: null, ageMs: null };
484
+ }
485
+ const age = Date.now() - _lastWrite;
486
+ return {
487
+ source: age < 200 ? "programmatic" : "typed",
488
+ method: "timing-heuristic",
489
+ lastWriteMs: _lastWrite,
490
+ ageMs: age,
491
+ };
492
+ },
493
+
494
+ /**
495
+ * Returns true if the clipboard was most recently written by a human
496
+ * (pasted or typed), not by kitClipboard.write().
497
+ * @returns {boolean}
498
+ */
499
+ wasPasted() {
500
+ return this.detectSource().source !== "programmatic";
501
+ },
502
+
503
+ /**
504
+ * Returns true if the clipboard was most recently written programmatically.
505
+ * @returns {boolean}
506
+ */
507
+ wasProgrammatic() {
508
+ return this.detectSource().source === "programmatic";
509
+ },
510
+
511
+ // ──────────────────────────────────────────────────────────────────────
512
+ // FORMAT DETECTION
513
+ // ──────────────────────────────────────────────────────────────────────
514
+
515
+ /**
516
+ * Detect the format/type of the current clipboard content.
517
+ * @returns {{ format: string, confidence: 'high'|'medium'|'low', details: object }}
518
+ *
519
+ * Possible formats: "json", "url", "email", "html", "csv", "code",
520
+ * "markdown", "base64", "hex", "uuid", "number", "date", "empty", "text"
521
+ */
522
+ detectFormat() {
523
+ const text = _read().trim();
524
+
525
+ if (!text) return { format: "empty", confidence: "high", details: {} };
526
+
527
+ // JSON
528
+ try {
529
+ const parsed = JSON.parse(text);
530
+ return { format: "json", confidence: "high", details: { type: typeof parsed, isArray: Array.isArray(parsed) } };
531
+ } catch {}
532
+
533
+ // UUID
534
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text)) {
535
+ return { format: "uuid", confidence: "high", details: { version: parseInt(text[14]) || null } };
536
+ }
537
+
538
+ // URL
539
+ try {
540
+ const url = new URL(text);
541
+ return { format: "url", confidence: "high", details: { protocol: url.protocol, host: url.host } };
542
+ } catch {}
543
+
544
+ // Email
545
+ if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text)) {
546
+ return { format: "email", confidence: "high", details: {} };
547
+ }
548
+
549
+ // Hex color
550
+ if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(text)) {
551
+ return { format: "hex-color", confidence: "high", details: { value: text } };
552
+ }
553
+
554
+ // Pure hex string
555
+ if (/^[0-9a-f]+$/i.test(text) && text.length % 2 === 0 && text.length >= 8) {
556
+ return { format: "hex", confidence: "medium", details: { bytes: text.length / 2 } };
557
+ }
558
+
559
+ // Base64
560
+ if (/^[A-Za-z0-9+/]+=*$/.test(text) && text.length % 4 === 0 && text.length >= 16) {
561
+ return { format: "base64", confidence: "medium", details: { decodedLength: Math.floor(text.length * 0.75) } };
562
+ }
563
+
564
+ // Number
565
+ if (!isNaN(Number(text)) && text.length > 0) {
566
+ return { format: "number", confidence: "high", details: { value: Number(text) } };
567
+ }
568
+
569
+ // Date
570
+ const d = new Date(text);
571
+ if (!isNaN(d.getTime()) && text.length > 5) {
572
+ return { format: "date", confidence: "medium", details: { iso: d.toISOString() } };
573
+ }
574
+
575
+ // HTML
576
+ if (/<[a-z][\s\S]*>/i.test(text)) {
577
+ return { format: "html", confidence: "medium", details: { tags: (text.match(/<[a-z][a-z0-9]*/gi) || []).slice(0, 5) } };
578
+ }
579
+
580
+ // CSV (has commas and newlines or consistent comma count per line)
581
+ const lines = text.split("\n").filter(Boolean);
582
+ if (lines.length > 1) {
583
+ const commasPerLine = lines.map(l => (l.match(/,/g) || []).length);
584
+ const consistent = commasPerLine.every(c => c === commasPerLine[0] && c > 0);
585
+ if (consistent) return { format: "csv", confidence: "medium", details: { rows: lines.length, cols: commasPerLine[0] + 1 } };
586
+ }
587
+
588
+ // Markdown (has #, **, __, -, ```)
589
+ if (/^#{1,6} |^\*\*|^__|^- |\`\`\`/m.test(text)) {
590
+ return { format: "markdown", confidence: "medium", details: {} };
591
+ }
592
+
593
+ // Code heuristic (has braces, semicolons, keywords)
594
+ const codeScore = [
595
+ /[{};]/.test(text),
596
+ /\b(function|const|let|var|if|for|while|return|import|export|class|def|fn|pub)\b/.test(text),
597
+ /\/\/|\/\*|#/.test(text),
598
+ ].filter(Boolean).length;
599
+ if (codeScore >= 2) {
600
+ return { format: "code", confidence: "medium", details: { codeScore } };
601
+ }
602
+
603
+ return { format: "text", confidence: "high", details: { length: text.length, lines: lines.length } };
604
+ },
605
+
606
+ /**
607
+ * Check if the clipboard contains a specific format.
608
+ * @param {'json'|'url'|'email'|'html'|'csv'|'code'|'markdown'|'base64'|'hex'|'uuid'|'number'|'date'|'empty'|'text'} format
609
+ * @returns {boolean}
610
+ */
611
+ isFormat(format) {
612
+ return this.detectFormat().format === format;
613
+ },
614
+
615
+ /**
616
+ * Check if the clipboard is empty.
617
+ * @returns {boolean}
618
+ */
619
+ isEmpty() {
620
+ return _read().trim().length === 0;
621
+ },
622
+
623
+ /**
624
+ * Get the byte size of the current clipboard content.
625
+ * @returns {number}
626
+ */
627
+ size() {
628
+ return Buffer.byteLength(_read(), "utf8");
629
+ },
630
+
631
+ /**
632
+ * Get the character count of the current clipboard content.
633
+ * @returns {number}
634
+ */
635
+ length() {
636
+ return _read().length;
637
+ },
638
+
639
+ /**
640
+ * Get the line count of the current clipboard content.
641
+ * @returns {number}
642
+ */
643
+ lineCount() {
644
+ return _read().split("\n").length;
645
+ },
646
+
647
+ /**
648
+ * Get the word count of the current clipboard content.
649
+ * @returns {number}
650
+ */
651
+ wordCount() {
652
+ return _read().trim().split(/\s+/).filter(Boolean).length;
653
+ },
654
+
655
+ // ──────────────────────────────────────────────────────────────────────
656
+ // TRANSFORM
657
+ // ──────────────────────────────────────────────────────────────────────
658
+
659
+ /**
660
+ * Transform the clipboard content with a function and write it back.
661
+ * @param {function} fn - (text: string) => string
662
+ * @returns {{ before: string, after: string }}
663
+ *
664
+ * @example
665
+ * kitClipboard.transform(text => text.toUpperCase());
666
+ */
667
+ transform(fn) {
668
+ const before = _read();
669
+ const after = fn(before);
670
+ this.write(after);
671
+ return { before, after };
672
+ },
673
+
674
+ /** Uppercase the clipboard content. */
675
+ toUpperCase() { return this.transform(t => t.toUpperCase()); },
676
+
677
+ /** Lowercase the clipboard content. */
678
+ toLowerCase() { return this.transform(t => t.toLowerCase()); },
679
+
680
+ /** Trim whitespace from clipboard content. */
681
+ trim() { return this.transform(t => t.trim()); },
682
+
683
+ /** Reverse the clipboard content. */
684
+ reverse() { return this.transform(t => t.split("").reverse().join("")); },
685
+
686
+ /** Sort lines in the clipboard alphabetically. */
687
+ sortLines() { return this.transform(t => t.split("\n").sort().join("\n")); },
688
+
689
+ /** Deduplicate lines in the clipboard. */
690
+ dedupeLines() { return this.transform(t => [...new Set(t.split("\n"))].join("\n")); },
691
+
692
+ /** URL-encode the clipboard content. */
693
+ urlEncode() { return this.transform(t => encodeURIComponent(t)); },
694
+
695
+ /** URL-decode the clipboard content. */
696
+ urlDecode() { return this.transform(t => decodeURIComponent(t)); },
697
+
698
+ /** Base64-encode the clipboard content. */
699
+ base64Encode() { return this.transform(t => Buffer.from(t).toString("base64")); },
700
+
701
+ /** Base64-decode the clipboard content. */
702
+ base64Decode() { return this.transform(t => Buffer.from(t, "base64").toString("utf8")); },
703
+
704
+ /** Strip all HTML tags from the clipboard content. */
705
+ stripHTML() { return this.transform(t => t.replace(/<[^>]+>/g, "")); },
706
+
707
+ /**
708
+ * Find and replace in clipboard content.
709
+ * @param {string|RegExp} search
710
+ * @param {string} replace
711
+ * @returns {{ before, after }}
712
+ */
713
+ replace(search, replace) {
714
+ return this.transform(t => t.replace(search, replace));
715
+ },
716
+
717
+ /**
718
+ * Find and replace all occurrences in clipboard content.
719
+ * @param {string|RegExp} search
720
+ * @param {string} replace
721
+ */
722
+ replaceAll(search, replace) {
723
+ return this.transform(t => t.replaceAll(search, replace));
724
+ },
725
+
726
+ /**
727
+ * Wrap clipboard content in a string on both sides.
728
+ * @param {string} prefix
729
+ * @param {string} [suffix] - Defaults to prefix.
730
+ */
731
+ wrap(prefix, suffix) {
732
+ return this.transform(t => `${prefix}${t}${suffix ?? prefix}`);
733
+ },
734
+
735
+ // ──────────────────────────────────────────────────────────────────────
736
+ // HISTORY
737
+ // ──────────────────────────────────────────────────────────────────────
738
+
739
+ /**
740
+ * Get the clipboard history (most recent first).
741
+ * History is tracked for writes made through kitClipboard.write() only.
742
+ * @param {number} [limit] - Max entries to return.
743
+ * @returns {Array<{ text, timestamp, source }>}
744
+ */
745
+ history(limit) {
746
+ return limit ? _history.slice(0, limit) : _history.slice();
747
+ },
748
+
749
+ /**
750
+ * Get a specific history entry by index (0 = most recent).
751
+ * @param {number} index
752
+ * @returns {{ text, timestamp, source } | undefined}
753
+ */
754
+ historyAt(index) {
755
+ return _history[index];
756
+ },
757
+
758
+ /**
759
+ * Restore a previous clipboard entry by index.
760
+ * @param {number} index
761
+ * @returns {{ restored: string }}
762
+ */
763
+ restore(index) {
764
+ const entry = _history[index];
765
+ if (!entry) throw new Error(`No history entry at index ${index}.`);
766
+ this.write(entry.text);
767
+ return { restored: entry.text };
768
+ },
769
+
770
+ /**
771
+ * Clear the clipboard history.
772
+ * @returns {{ cleared: number }}
773
+ */
774
+ clearHistory() {
775
+ const count = _history.length;
776
+ _history.length = 0;
777
+ return { cleared: count };
778
+ },
779
+
780
+ /**
781
+ * Search history for entries matching a string or regex.
782
+ * @param {string|RegExp} query
783
+ * @returns {Array<{ index, text, timestamp, source }>}
784
+ */
785
+ searchHistory(query) {
786
+ return _history
787
+ .map((entry, index) => ({ index, ...entry }))
788
+ .filter(e => typeof query === "string"
789
+ ? e.text.includes(query)
790
+ : query.test(e.text));
791
+ },
792
+
793
+ // ──────────────────────────────────────────────────────────────────────
794
+ // MONITORING / WATCHING
795
+ // ──────────────────────────────────────────────────────────────────────
796
+
797
+ /**
798
+ * Watch the clipboard for changes and call a callback when it changes.
799
+ * @param {function} onChange - Called with { text, previous, timestamp, format }
800
+ * @param {number} [intervalMs=500] - Polling interval.
801
+ * @returns {{ stop: function }}
802
+ *
803
+ * @example
804
+ * const watcher = kitClipboard.watch(({ text }) => console.log("Clipboard:", text));
805
+ * // later:
806
+ * watcher.stop();
807
+ */
808
+ watch(onChange, intervalMs = 500) {
809
+ if (_watchTimer) clearInterval(_watchTimer);
810
+ _watchLast = _read();
811
+
812
+ _watchTimer = setInterval(() => {
813
+ try {
814
+ const current = _read();
815
+ if (current !== _watchLast) {
816
+ const previous = _watchLast;
817
+ _watchLast = current;
818
+ _pushHistory(current, "external");
819
+ onChange({
820
+ text: current,
821
+ previous,
822
+ timestamp: Date.now(),
823
+ format: this.detectFormat().format,
824
+ });
825
+ }
826
+ } catch { /* clipboard may be temporarily unavailable */ }
827
+ }, intervalMs);
828
+
829
+ return { stop: () => { clearInterval(_watchTimer); _watchTimer = null; } };
830
+ },
831
+
832
+ /**
833
+ * Wait for the clipboard to change and resolve with the new value.
834
+ * @param {number} [timeoutMs=30000]
835
+ * @returns {Promise<{ text, previous, format }>}
836
+ */
837
+ waitForChange(timeoutMs = 30000) {
838
+ return new Promise((resolve, reject) => {
839
+ const initial = _read();
840
+ const start = Date.now();
841
+
842
+ const timer = setInterval(() => {
843
+ if (Date.now() - start > timeoutMs) {
844
+ clearInterval(timer);
845
+ reject(new Error("Clipboard watch timed out."));
846
+ return;
847
+ }
848
+ try {
849
+ const current = _read();
850
+ if (current !== initial) {
851
+ clearInterval(timer);
852
+ resolve({ text: current, previous: initial, format: this.detectFormat().format });
853
+ }
854
+ } catch {}
855
+ }, 200);
856
+ });
857
+ },
858
+
859
+ /**
860
+ * Stop any active clipboard watcher.
861
+ * @returns {{ stopped: boolean }}
862
+ */
863
+ stopWatch() {
864
+ if (_watchTimer) { clearInterval(_watchTimer); _watchTimer = null; return { stopped: true }; }
865
+ return { stopped: false };
866
+ },
867
+
868
+ // ──────────────────────────────────────────────────────────────────────
869
+ // PLATFORM & DIAGNOSTICS
870
+ // ──────────────────────────────────────────────────────────────────────
871
+
872
+ /**
873
+ * Get information about the current platform and clipboard backend.
874
+ * @returns {object}
875
+ */
876
+ platformInfo() {
877
+ const plat = _platform();
878
+ let backend = "native";
879
+ if (["x11", "wayland", "linux"].includes(plat)) {
880
+ try { backend = _linuxBackend(); } catch { backend = "unavailable"; }
881
+ }
882
+ return {
883
+ platform: plat,
884
+ backend,
885
+ nodeVersion: process.version,
886
+ supported: ["macos", "windows", "android", "x11", "wayland", "linux"].includes(plat),
887
+ };
888
+ },
889
+
890
+ /**
891
+ * Check if the clipboard is accessible (backend available).
892
+ * @returns {boolean}
893
+ */
894
+ isAvailable() {
895
+ try { _read(); return true; }
896
+ catch { return false; }
897
+ },
898
+
899
+ /**
900
+ * Run a full diagnostic on the clipboard system.
901
+ * @returns {object}
902
+ */
903
+ diagnose() {
904
+ const info = this.platformInfo();
905
+ let canRead = false;
906
+ let canWrite = false;
907
+ let readValue = null;
908
+
909
+ try { readValue = _read(); canRead = true; } catch {}
910
+ try { _write("kitClipboard-diagnostic-test"); canWrite = true; } catch {}
911
+ // Restore
912
+ if (readValue !== null) try { _write(readValue); } catch {}
913
+
914
+ return {
915
+ ...info,
916
+ canRead,
917
+ canWrite,
918
+ historyEntries: _history.length,
919
+ watcherActive: _watchTimer !== null,
920
+ lastWriteMs: _lastWrite,
921
+ };
922
+ },
923
+
924
+ }
925
+ };