pi-lxmf 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/CHANGELOG.md +111 -0
- package/README.md +202 -0
- package/SPEC.md +469 -0
- package/package.json +52 -0
- package/src/bin.js +220 -0
- package/src/bridge.js +622 -0
- package/src/bz2.js +46 -0
- package/src/commands.js +288 -0
- package/src/config.js +393 -0
- package/src/identity.js +93 -0
- package/src/lxmf.js +380 -0
- package/src/quota.js +512 -0
- package/src/rpc.js +627 -0
- package/src/text.js +97 -0
package/src/text.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file text.js
|
|
3
|
+
*
|
|
4
|
+
* Pure text helpers shared by the bridge and the LXMF sender: reply
|
|
5
|
+
* chunking and human-friendly formatting. Kept dependency-free so tests
|
|
6
|
+
* stay light.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Splits `text` into chunks of at most `maxChars` characters, preferring
|
|
11
|
+
* paragraph (`\n\n`) boundaries, then single newlines, then hard cuts. Only
|
|
12
|
+
* the *content* is measured; the `[… n/N]` markers are appended by the
|
|
13
|
+
* caller (the sender) so chunk sizes stay predictable here.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} text
|
|
16
|
+
* @param {number} maxChars - Maximum characters per chunk (>= 1).
|
|
17
|
+
* @returns {string[]}
|
|
18
|
+
*/
|
|
19
|
+
export function chunkText(text, maxChars) {
|
|
20
|
+
if (!Number.isFinite(maxChars) || maxChars < 1) {
|
|
21
|
+
throw new Error(`maxChars must be a positive number, got ${maxChars}`);
|
|
22
|
+
}
|
|
23
|
+
const trimmed = text.trim();
|
|
24
|
+
if (trimmed.length === 0) return [];
|
|
25
|
+
if (trimmed.length <= maxChars) return [trimmed];
|
|
26
|
+
|
|
27
|
+
/** @type {string[]} */
|
|
28
|
+
const chunks = [];
|
|
29
|
+
let rest = trimmed;
|
|
30
|
+
while (rest.length > maxChars) {
|
|
31
|
+
const window = rest.slice(0, maxChars);
|
|
32
|
+
let cut = window.lastIndexOf("\n\n");
|
|
33
|
+
if (cut < maxChars * 0.5) cut = window.lastIndexOf("\n");
|
|
34
|
+
if (cut < maxChars * 0.5) cut = window.lastIndexOf(" ");
|
|
35
|
+
if (cut < maxChars * 0.5) cut = -1;
|
|
36
|
+
if (cut === -1) {
|
|
37
|
+
chunks.push(window);
|
|
38
|
+
rest = rest.slice(maxChars);
|
|
39
|
+
} else {
|
|
40
|
+
chunks.push(window.slice(0, cut));
|
|
41
|
+
rest = rest.slice(cut).replace(/^\n+/, "");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (rest.length > 0) chunks.push(rest);
|
|
45
|
+
return chunks.filter((c) => c.length > 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Formats an unknown thrown value as a short message.
|
|
50
|
+
*
|
|
51
|
+
* @param {unknown} e
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function errorText(e) {
|
|
55
|
+
if (e instanceof Error) return e.message;
|
|
56
|
+
return String(e);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Formats a duration in milliseconds as a compact human string
|
|
61
|
+
* (`3d 4h 12m`, `4h 12m`, `12m 5s`, `8s`).
|
|
62
|
+
*
|
|
63
|
+
* @param {number} ms
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function formatDuration(ms) {
|
|
67
|
+
if (!Number.isFinite(ms) || ms < 0) return "0s";
|
|
68
|
+
const s = Math.floor(ms / 1000);
|
|
69
|
+
const parts = [
|
|
70
|
+
{ unit: "d", n: Math.floor(s / 86400) },
|
|
71
|
+
{ unit: "h", n: Math.floor((s % 86400) / 3600) },
|
|
72
|
+
{ unit: "m", n: Math.floor((s % 3600) / 60) },
|
|
73
|
+
{ unit: "s", n: s % 60 },
|
|
74
|
+
];
|
|
75
|
+
const nonzero = parts.filter((p) => p.n > 0);
|
|
76
|
+
const kept = nonzero.slice(0, 2);
|
|
77
|
+
if (kept.length === 0) return "0s";
|
|
78
|
+
return kept.map((p) => `${p.n}${p.unit}`).join(" ");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Formats a token/cost tally as returned by the `get_session_stats` RPC.
|
|
83
|
+
*
|
|
84
|
+
* @param {{tokens?: {total?: number|null}, cost?: number|null, contextUsage?: {percent?: number|null}|null}} stats
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function formatTokens(stats) {
|
|
88
|
+
const tokens = stats?.tokens?.total;
|
|
89
|
+
const cost = stats?.cost;
|
|
90
|
+
const percent = stats?.contextUsage?.percent;
|
|
91
|
+
const bits = [];
|
|
92
|
+
if (typeof tokens === "number")
|
|
93
|
+
bits.push(`${tokens.toLocaleString()} tokens`);
|
|
94
|
+
if (typeof cost === "number") bits.push(`$${cost.toFixed(2)}`);
|
|
95
|
+
if (typeof percent === "number") bits.push(`context ${Math.round(percent)}%`);
|
|
96
|
+
return bits.length > 0 ? bits.join(", ") : "no usage recorded";
|
|
97
|
+
}
|