u-foo 1.0.6 → 1.1.9

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 (149) hide show
  1. package/README.md +44 -4
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +11 -2
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +154 -0
  64. package/src/chat/index.js +935 -2909
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +132 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1580 -0
  98. package/src/config.js +47 -1
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +661 -488
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -0,0 +1,245 @@
1
+ const EventEmitter = require("events");
2
+ const WebSocket = require("ws");
3
+ const { loadTokens, defaultTokensPath } = require("./tokens");
4
+
5
+ function waitForOpen(ws, timeoutMs = 5000) {
6
+ if (ws.readyState === WebSocket.OPEN) return Promise.resolve();
7
+ return new Promise((resolve, reject) => {
8
+ const timer = setTimeout(() => reject(new Error("WebSocket open timeout")), timeoutMs);
9
+ ws.once("open", () => {
10
+ clearTimeout(timer);
11
+ resolve();
12
+ });
13
+ ws.once("error", (err) => {
14
+ clearTimeout(timer);
15
+ reject(err);
16
+ });
17
+ });
18
+ }
19
+
20
+ class OnlineClient extends EventEmitter {
21
+ constructor(options = {}) {
22
+ super();
23
+ this.url = options.url || "ws://127.0.0.1:8787/ufoo/online";
24
+ this.subscriberId = options.subscriberId || "";
25
+ this.nickname = options.nickname || "";
26
+ this.world = options.world || "default";
27
+ this.agentType = options.agentType || "";
28
+ this.version = options.version || "0.1.0";
29
+ this.capabilities = Array.isArray(options.capabilities) ? options.capabilities : [];
30
+ this.project = options.project || null;
31
+
32
+ this.token = options.token || "";
33
+ this.tokenHash = options.tokenHash || "";
34
+ this.tokenFile = options.tokenFile || defaultTokensPath();
35
+ this.allowInsecureWs = options.allowInsecureWs || false;
36
+
37
+ this.ws = null;
38
+ this.connected = false;
39
+ }
40
+
41
+ resolveToken() {
42
+ if (this.token || this.tokenHash) return;
43
+ if (!this.subscriberId) return;
44
+ const data = loadTokens(this.tokenFile);
45
+ const entry = data.agents?.[this.subscriberId];
46
+ if (!entry) return;
47
+ this.token = entry.token || this.token;
48
+ this.tokenHash = entry.token_hash || this.tokenHash;
49
+ if (!this.nickname && entry.nickname) this.nickname = entry.nickname;
50
+ if (!this.url && entry.server) this.url = entry.server;
51
+ }
52
+
53
+ buildHello() {
54
+ return {
55
+ type: "hello",
56
+ client: {
57
+ subscriber_id: this.subscriberId,
58
+ agent_type: this.agentType,
59
+ nickname: this.nickname,
60
+ world: this.world,
61
+ version: this.version,
62
+ capabilities: this.capabilities,
63
+ project: this.project || undefined,
64
+ },
65
+ };
66
+ }
67
+
68
+ buildAuth() {
69
+ const payload = { type: "auth", method: "token" };
70
+ if (this.tokenHash) payload.token_hash = this.tokenHash;
71
+ else payload.token = this.token;
72
+ return payload;
73
+ }
74
+
75
+ async connect({ timeoutMs = 6000 } = {}) {
76
+ if (this.connected) return;
77
+ this.resolveToken();
78
+ if (!this.subscriberId || !this.nickname) {
79
+ throw new Error("subscriberId and nickname are required");
80
+ }
81
+ if (!this.token && !this.tokenHash) {
82
+ throw new Error("token (or token_hash) is required");
83
+ }
84
+
85
+ // Step 10: Enforce TLS for non-local ws:// unless explicitly allowed
86
+ if (this.url.startsWith("ws://")) {
87
+ let parsed;
88
+ try {
89
+ parsed = new URL(this.url);
90
+ } catch {
91
+ throw new Error(`Invalid WebSocket URL: ${this.url}`);
92
+ }
93
+ const host = parsed.hostname;
94
+ const isLocal = host === "127.0.0.1" || host === "localhost" || host === "::1";
95
+ if (!isLocal) {
96
+ if (!this.allowInsecureWs) {
97
+ throw new Error(
98
+ `Refusing to connect to non-localhost over unencrypted ws:// ("${host}"). Use wss:// or pass --allow-insecure-ws.`
99
+ );
100
+ }
101
+ this.emit(
102
+ "warning",
103
+ `Connecting over unencrypted ws:// to non-localhost host "${host}". Consider using wss:// with TLS.`
104
+ );
105
+ }
106
+ }
107
+
108
+ this.ws = new WebSocket(this.url);
109
+ await waitForOpen(this.ws, timeoutMs);
110
+
111
+ this.ws.on("message", (data) => {
112
+ try {
113
+ const msg = JSON.parse(data.toString());
114
+ this.emit("message", msg);
115
+ if (msg.type === "wake") {
116
+ this.emit("wake", msg);
117
+ }
118
+ } catch {
119
+ // ignore
120
+ }
121
+ });
122
+
123
+ this.ws.on("close", () => {
124
+ this.connected = false;
125
+ this.emit("close");
126
+ });
127
+
128
+ this.ws.on("error", (err) => {
129
+ this.emit("error", err);
130
+ });
131
+
132
+ const next = this.createMessageQueue();
133
+
134
+ this.send(this.buildHello());
135
+ const helloAck = await next(timeoutMs);
136
+ if (helloAck.type === "error") throw new Error(helloAck.error || "hello failed");
137
+
138
+ let authRequired = null;
139
+ try {
140
+ authRequired = await next(timeoutMs);
141
+ } catch {
142
+ authRequired = null;
143
+ }
144
+ if (!authRequired || authRequired.type !== "auth_required") {
145
+ authRequired = { type: "auth_required" };
146
+ }
147
+
148
+ this.send(this.buildAuth());
149
+ const authOk = await next(timeoutMs);
150
+ if (authOk.type !== "auth_ok") {
151
+ throw new Error(authOk.error || "auth failed");
152
+ }
153
+
154
+ if (typeof next.cleanup === "function") {
155
+ next.cleanup();
156
+ }
157
+
158
+ this.connected = true;
159
+ }
160
+
161
+ createMessageQueue() {
162
+ const messages = [];
163
+ let resolver = null;
164
+
165
+ const handler = (msg) => {
166
+ if (resolver) {
167
+ const next = resolver;
168
+ resolver = null;
169
+ next(msg);
170
+ } else {
171
+ messages.push(msg);
172
+ }
173
+ };
174
+
175
+ const onMessage = (data) => {
176
+ try {
177
+ handler(JSON.parse(data.toString()));
178
+ } catch {
179
+ // ignore
180
+ }
181
+ };
182
+
183
+ this.ws.on("message", onMessage);
184
+
185
+ const next = (timeoutMs = 3000) =>
186
+ new Promise((resolve, reject) => {
187
+ if (messages.length > 0) return resolve(messages.shift());
188
+ resolver = resolve;
189
+ setTimeout(() => {
190
+ if (resolver === resolve) {
191
+ resolver = null;
192
+ reject(new Error("Timeout waiting for message"));
193
+ }
194
+ }, timeoutMs);
195
+ });
196
+
197
+ next.cleanup = () => {
198
+ this.ws.off("message", onMessage);
199
+ };
200
+
201
+ return next;
202
+ }
203
+
204
+ send(payload) {
205
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
206
+ try {
207
+ this.ws.send(JSON.stringify(payload));
208
+ return true;
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+
214
+ join(channel) {
215
+ this.send({ type: "join", channel });
216
+ }
217
+
218
+ leave(channel) {
219
+ this.send({ type: "leave", channel });
220
+ }
221
+
222
+ joinRoom(room, password = "") {
223
+ this.send({ type: "join", room, password });
224
+ }
225
+
226
+ leaveRoom(room) {
227
+ this.send({ type: "leave", room });
228
+ }
229
+
230
+ sendEvent({ channel, room, payload }) {
231
+ return this.send({ type: "event", channel, room, payload });
232
+ }
233
+
234
+ close() {
235
+ if (this.ws) {
236
+ try {
237
+ this.ws.terminate();
238
+ } catch {
239
+ // ignore
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ module.exports = OnlineClient;
@@ -0,0 +1,253 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function inboxDir() {
5
+ return path.join(
6
+ process.env.HOME || process.env.USERPROFILE,
7
+ ".ufoo",
8
+ "online",
9
+ "inbox"
10
+ );
11
+ }
12
+
13
+ function inboxFilePath(nickname) {
14
+ return path.join(inboxDir(), `${nickname}.jsonl`);
15
+ }
16
+
17
+ function readMarkerPath(nickname) {
18
+ return path.join(inboxDir(), `${nickname}.read`);
19
+ }
20
+
21
+ function outboxFilePath(nickname) {
22
+ const dir = path.join(
23
+ process.env.HOME || process.env.USERPROFILE,
24
+ ".ufoo",
25
+ "online",
26
+ "outbox"
27
+ );
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ return path.join(dir, `${nickname}.jsonl`);
30
+ }
31
+
32
+ function send(nickname, options = {}) {
33
+ if (!nickname) {
34
+ console.error("nickname is required");
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+ const text = options.text || "";
39
+ if (!text) {
40
+ console.error("--text is required");
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ const msg = { text };
45
+ if (options.channel) msg.channel = options.channel;
46
+ if (options.room) msg.room = options.room;
47
+ if (!msg.channel && !msg.room) {
48
+ console.error("--channel or --room is required");
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+
53
+ const file = outboxFilePath(nickname);
54
+ fs.appendFileSync(file, JSON.stringify(msg) + "\n");
55
+ const target = msg.channel ? `channel ${msg.channel}` : `room ${msg.room}`;
56
+ console.log(`Queued to ${target}: ${text}`);
57
+ }
58
+
59
+ const RETENTION_MS = {
60
+ channel: 7 * 24 * 60 * 60 * 1000, // 7 days
61
+ room: 30 * 24 * 60 * 60 * 1000, // 30 days
62
+ };
63
+
64
+ function cleanupInbox(file) {
65
+ if (!fs.existsSync(file)) return;
66
+ const now = Date.now();
67
+ const lines = fs.readFileSync(file, "utf-8").split("\n").filter(Boolean);
68
+ const kept = [];
69
+ for (const line of lines) {
70
+ let msg;
71
+ try { msg = JSON.parse(line); } catch { continue; }
72
+ const source = msg._source || "channel";
73
+ const maxAge = RETENTION_MS[source];
74
+ const age = now - new Date(msg._receivedAt || 0).getTime();
75
+ if (age < maxAge) kept.push(line);
76
+ }
77
+ fs.writeFileSync(file, kept.length ? kept.join("\n") + "\n" : "");
78
+ }
79
+
80
+ function checkInbox(nickname, options = {}) {
81
+ const clear = options.clear || false;
82
+ const unreadOnly = options.unread || false;
83
+
84
+ if (!nickname) {
85
+ console.error("nickname is required");
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+
90
+ const file = inboxFilePath(nickname);
91
+ const markerFile = readMarkerPath(nickname);
92
+
93
+ if (clear) {
94
+ if (fs.existsSync(file)) {
95
+ fs.unlinkSync(file);
96
+ console.log(`Inbox cleared for ${nickname}.`);
97
+ } else {
98
+ console.log(`No inbox file for ${nickname}.`);
99
+ }
100
+ return;
101
+ }
102
+
103
+ cleanupInbox(file);
104
+
105
+ let messages = [];
106
+ if (fs.existsSync(file)) {
107
+ const lines = fs.readFileSync(file, "utf-8").split("\n").filter(Boolean);
108
+ for (const line of lines) {
109
+ try {
110
+ messages.push(JSON.parse(line));
111
+ } catch {
112
+ // skip malformed
113
+ }
114
+ }
115
+ }
116
+
117
+ function displayWidth(str) {
118
+ let w = 0;
119
+ for (const ch of str) {
120
+ const cp = ch.codePointAt(0);
121
+ if (
122
+ (cp >= 0x1100 && cp <= 0x115f) ||
123
+ (cp >= 0x2e80 && cp <= 0x303e) ||
124
+ (cp >= 0x3040 && cp <= 0x33bf) ||
125
+ (cp >= 0x3400 && cp <= 0x4dbf) ||
126
+ (cp >= 0x4e00 && cp <= 0xa4cf) ||
127
+ (cp >= 0xac00 && cp <= 0xd7af) ||
128
+ (cp >= 0xf900 && cp <= 0xfaff) ||
129
+ (cp >= 0xfe30 && cp <= 0xfe6f) ||
130
+ (cp >= 0xff01 && cp <= 0xff60) ||
131
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
132
+ (cp >= 0x1f300 && cp <= 0x1f9ff) ||
133
+ (cp >= 0x20000 && cp <= 0x2fa1f)
134
+ ) {
135
+ w += 2;
136
+ } else {
137
+ w += 1;
138
+ }
139
+ }
140
+ return w;
141
+ }
142
+
143
+ function padRight(str, width) {
144
+ const dw = displayWidth(str);
145
+ if (dw >= width) return str;
146
+ return str + " ".repeat(width - dw);
147
+ }
148
+
149
+ const W = 50;
150
+
151
+ function hline(left, fill, right) {
152
+ return left + fill.repeat(W) + right;
153
+ }
154
+
155
+ function row(text) {
156
+ return "\u2551 " + padRight(text, W - 2) + " \u2551";
157
+ }
158
+
159
+ let lastRead = "";
160
+ try {
161
+ lastRead = fs.readFileSync(markerFile, "utf-8").trim();
162
+ } catch {
163
+ // no marker
164
+ }
165
+
166
+ const unreadMessages = lastRead
167
+ ? messages.filter((m) => (m._receivedAt || "") > lastRead)
168
+ : messages;
169
+
170
+ const displayMessages = unreadOnly ? unreadMessages : messages;
171
+ const count = displayMessages.length;
172
+ const unreadCount = unreadMessages.length;
173
+ const label = unreadOnly ? "unread" : "message";
174
+ const unreadTag = unreadCount > 0 ? `, ${unreadCount} unread` : "";
175
+ const title = `\ud83d\udcec Inbox: ${nickname} (${count} ${label}${count !== 1 ? "s" : ""}${unreadOnly ? "" : unreadTag})`;
176
+
177
+ console.log(hline("\u2554", "\u2550", "\u2557"));
178
+ console.log(row(title));
179
+
180
+ if (count === 0) {
181
+ console.log(hline("\u2560", "\u2550", "\u2563"));
182
+ console.log(row("(empty)"));
183
+ console.log(hline("\u255a", "\u2550", "\u255d"));
184
+ markAsRead(markerFile);
185
+ return;
186
+ }
187
+
188
+ displayMessages.forEach((msg, i) => {
189
+ console.log(hline("\u2560", "\u2550", "\u2563"));
190
+
191
+ const from = msg.from || msg.subscriberId || "unknown";
192
+ const time = msg._receivedAt
193
+ ? msg._receivedAt.replace("T", " ").replace(/\.\d+Z$/, "")
194
+ : "?";
195
+ const isUnread = !lastRead || (msg._receivedAt || "") > lastRead;
196
+ const marker = isUnread ? " [NEW]" : "";
197
+
198
+ const source = msg._source || "channel";
199
+ const sourceTag = source === "room"
200
+ ? ` [${msg.room || "room"}]`
201
+ : ` [${msg.channel || "channel"}]`;
202
+
203
+ console.log(row(`#${i + 1} from: ${from}${sourceTag}${marker}`));
204
+ console.log(row(` time: ${time}`));
205
+ console.log(row(" " + "\u2500".repeat(Math.min(W - 6, 37))));
206
+
207
+ let body = "";
208
+ if (msg.payload) {
209
+ if (typeof msg.payload === "string") {
210
+ body = msg.payload;
211
+ } else if (msg.payload.message) {
212
+ body = msg.payload.message;
213
+ } else {
214
+ body = JSON.stringify(msg.payload);
215
+ }
216
+ } else {
217
+ body = JSON.stringify(msg);
218
+ }
219
+
220
+ const maxLine = W - 6;
221
+ const bodyLines = [];
222
+ for (const rawLine of body.split("\n")) {
223
+ if (displayWidth(rawLine) <= maxLine) {
224
+ bodyLines.push(rawLine);
225
+ } else {
226
+ let cur = "";
227
+ for (const ch of rawLine) {
228
+ if (displayWidth(cur + ch) > maxLine) {
229
+ bodyLines.push(cur);
230
+ cur = ch;
231
+ } else {
232
+ cur += ch;
233
+ }
234
+ }
235
+ if (cur) bodyLines.push(cur);
236
+ }
237
+ }
238
+
239
+ for (const line of bodyLines) {
240
+ console.log(row(" " + line));
241
+ }
242
+ });
243
+
244
+ console.log(hline("\u255a", "\u2550", "\u255d"));
245
+ markAsRead(markerFile);
246
+ }
247
+
248
+ function markAsRead(markerFile) {
249
+ fs.mkdirSync(path.dirname(markerFile), { recursive: true });
250
+ fs.writeFileSync(markerFile, new Date().toISOString());
251
+ }
252
+
253
+ module.exports = { send, checkInbox };