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.
- package/README.md +44 -4
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +11 -2
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +935 -2909
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +47 -1
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +661 -488
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- 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 };
|