@nordbyte/nordrelay 0.2.1
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/.env.example +88 -0
- package/Dockerfile +19 -0
- package/LICENSE +21 -0
- package/README.md +749 -0
- package/dist/access-control.js +146 -0
- package/dist/agent-factory.js +22 -0
- package/dist/agent.js +57 -0
- package/dist/artifacts.js +515 -0
- package/dist/attachments.js +69 -0
- package/dist/bot-preferences.js +146 -0
- package/dist/bot-ui.js +161 -0
- package/dist/bot.js +4520 -0
- package/dist/codex-auth.js +150 -0
- package/dist/codex-cli.js +79 -0
- package/dist/codex-config.js +50 -0
- package/dist/codex-launch.js +109 -0
- package/dist/codex-session.js +591 -0
- package/dist/codex-state.js +573 -0
- package/dist/config.js +385 -0
- package/dist/context-key.js +23 -0
- package/dist/error-messages.js +73 -0
- package/dist/format.js +121 -0
- package/dist/index.js +140 -0
- package/dist/logger.js +27 -0
- package/dist/operations.js +133 -0
- package/dist/persistence.js +65 -0
- package/dist/pi-cli.js +19 -0
- package/dist/pi-rpc.js +158 -0
- package/dist/pi-session.js +573 -0
- package/dist/pi-state.js +226 -0
- package/dist/prompt-store.js +241 -0
- package/dist/redaction.js +47 -0
- package/dist/session-format.js +191 -0
- package/dist/session-registry.js +195 -0
- package/dist/telegram-rate-limit.js +136 -0
- package/dist/voice.js +373 -0
- package/dist/workspace-policy.js +41 -0
- package/docker-compose.yml +17 -0
- package/launchd/start.sh +8 -0
- package/package.json +69 -0
- package/plugins/nordrelay/.codex-plugin/plugin.json +48 -0
- package/plugins/nordrelay/assets/nordrelay.svg +5 -0
- package/plugins/nordrelay/commands/remote.md +33 -0
- package/plugins/nordrelay/scripts/nordrelay.mjs +396 -0
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +26 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readJsonFileWithBackup, writeJsonFileAtomic } from "./persistence.js";
|
|
3
|
+
export class BotPreferencesStore {
|
|
4
|
+
persistPath;
|
|
5
|
+
contexts = new Map();
|
|
6
|
+
constructor(workspace) {
|
|
7
|
+
this.persistPath = path.join(workspace, ".nordrelay", "preferences.json");
|
|
8
|
+
this.load();
|
|
9
|
+
}
|
|
10
|
+
get(contextKey) {
|
|
11
|
+
return { ...(this.contexts.get(contextKey) ?? {}) };
|
|
12
|
+
}
|
|
13
|
+
update(contextKey, patch) {
|
|
14
|
+
const current = this.contexts.get(contextKey) ?? {};
|
|
15
|
+
const next = pruneEmptyPreferences({
|
|
16
|
+
...current,
|
|
17
|
+
...patch,
|
|
18
|
+
});
|
|
19
|
+
this.contexts.set(contextKey, next);
|
|
20
|
+
this.persist();
|
|
21
|
+
return { ...next };
|
|
22
|
+
}
|
|
23
|
+
clear(contextKey) {
|
|
24
|
+
this.contexts.delete(contextKey);
|
|
25
|
+
this.persist();
|
|
26
|
+
}
|
|
27
|
+
persist() {
|
|
28
|
+
const payload = {
|
|
29
|
+
version: 1,
|
|
30
|
+
contexts: Object.fromEntries(this.contexts.entries()),
|
|
31
|
+
};
|
|
32
|
+
writeJsonFileAtomic(this.persistPath, payload);
|
|
33
|
+
}
|
|
34
|
+
load() {
|
|
35
|
+
const result = readJsonFileWithBackup(this.persistPath);
|
|
36
|
+
if (!result.value?.contexts || typeof result.value.contexts !== "object") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const [contextKey, rawPreferences] of Object.entries(result.value.contexts)) {
|
|
40
|
+
const preferences = normalizePreferences(rawPreferences);
|
|
41
|
+
if (preferences) {
|
|
42
|
+
this.contexts.set(contextKey, preferences);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function parseMirrorMode(value, fallback) {
|
|
48
|
+
const normalized = value?.trim().toLowerCase();
|
|
49
|
+
if (normalized === "off" || normalized === "status" || normalized === "final" || normalized === "full") {
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
export function parseNotifyMode(value, fallback) {
|
|
55
|
+
const normalized = value?.trim().toLowerCase();
|
|
56
|
+
if (normalized === "off" || normalized === "minimal" || normalized === "all") {
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
export function parseVoiceBackendPreference(value) {
|
|
62
|
+
const normalized = value?.trim().toLowerCase();
|
|
63
|
+
if (normalized === "parakeet" ||
|
|
64
|
+
normalized === "faster-whisper" ||
|
|
65
|
+
normalized === "openai") {
|
|
66
|
+
return normalized;
|
|
67
|
+
}
|
|
68
|
+
return "auto";
|
|
69
|
+
}
|
|
70
|
+
export function parseQuietHours(value) {
|
|
71
|
+
if (!value) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const match = value.trim().match(/^(\d{1,2})\s*-\s*(\d{1,2})$/);
|
|
75
|
+
if (!match) {
|
|
76
|
+
throw new Error("Quiet hours must use HH-HH format, e.g. 22-7");
|
|
77
|
+
}
|
|
78
|
+
const startHour = Number(match[1]);
|
|
79
|
+
const endHour = Number(match[2]);
|
|
80
|
+
if (!Number.isInteger(startHour) || !Number.isInteger(endHour) || startHour < 0 || startHour > 23 || endHour < 0 || endHour > 23) {
|
|
81
|
+
throw new Error("Quiet hours must use hours from 0 to 23");
|
|
82
|
+
}
|
|
83
|
+
return { startHour, endHour };
|
|
84
|
+
}
|
|
85
|
+
export function isQuietNow(quietHours, now = new Date()) {
|
|
86
|
+
if (!quietHours) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const hour = now.getHours();
|
|
90
|
+
if (quietHours.startHour === quietHours.endHour) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (quietHours.startHour < quietHours.endHour) {
|
|
94
|
+
return hour >= quietHours.startHour && hour < quietHours.endHour;
|
|
95
|
+
}
|
|
96
|
+
return hour >= quietHours.startHour || hour < quietHours.endHour;
|
|
97
|
+
}
|
|
98
|
+
export function formatQuietHours(quietHours) {
|
|
99
|
+
if (!quietHours) {
|
|
100
|
+
return "off";
|
|
101
|
+
}
|
|
102
|
+
return `${quietHours.startHour.toString().padStart(2, "0")}-${quietHours.endHour.toString().padStart(2, "0")}`;
|
|
103
|
+
}
|
|
104
|
+
function normalizePreferences(value) {
|
|
105
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const candidate = value;
|
|
109
|
+
return pruneEmptyPreferences({
|
|
110
|
+
mirrorMode: isMirrorMode(candidate.mirrorMode) ? candidate.mirrorMode : undefined,
|
|
111
|
+
notifyMode: isNotifyMode(candidate.notifyMode) ? candidate.notifyMode : undefined,
|
|
112
|
+
quietHours: normalizeQuietHours(candidate.quietHours),
|
|
113
|
+
voiceBackend: isVoiceBackendPreference(candidate.voiceBackend) ? candidate.voiceBackend : undefined,
|
|
114
|
+
voiceLanguage: typeof candidate.voiceLanguage === "string" ? candidate.voiceLanguage : candidate.voiceLanguage === null ? null : undefined,
|
|
115
|
+
voiceTranscribeOnly: typeof candidate.voiceTranscribeOnly === "boolean" ? candidate.voiceTranscribeOnly : undefined,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function pruneEmptyPreferences(preferences) {
|
|
119
|
+
return Object.fromEntries(Object.entries(preferences).filter(([, value]) => value !== undefined));
|
|
120
|
+
}
|
|
121
|
+
function normalizeQuietHours(value) {
|
|
122
|
+
if (value === null) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const candidate = value;
|
|
129
|
+
return Number.isInteger(candidate.startHour) &&
|
|
130
|
+
Number.isInteger(candidate.endHour) &&
|
|
131
|
+
candidate.startHour >= 0 &&
|
|
132
|
+
candidate.startHour <= 23 &&
|
|
133
|
+
candidate.endHour >= 0 &&
|
|
134
|
+
candidate.endHour <= 23
|
|
135
|
+
? { startHour: candidate.startHour, endHour: candidate.endHour }
|
|
136
|
+
: undefined;
|
|
137
|
+
}
|
|
138
|
+
function isMirrorMode(value) {
|
|
139
|
+
return value === "off" || value === "status" || value === "final" || value === "full";
|
|
140
|
+
}
|
|
141
|
+
function isNotifyMode(value) {
|
|
142
|
+
return value === "off" || value === "minimal" || value === "all";
|
|
143
|
+
}
|
|
144
|
+
function isVoiceBackendPreference(value) {
|
|
145
|
+
return value === "auto" || value === "parakeet" || value === "faster-whisper" || value === "openai";
|
|
146
|
+
}
|
package/dist/bot-ui.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { escapeHTML } from "./format.js";
|
|
2
|
+
/**
|
|
3
|
+
* Grouped command reference for /help.
|
|
4
|
+
*/
|
|
5
|
+
export function renderHelpMessage() {
|
|
6
|
+
const sections = [
|
|
7
|
+
{
|
|
8
|
+
title: "๐ฌ Session",
|
|
9
|
+
commands: [
|
|
10
|
+
["/new", "Start a new thread"],
|
|
11
|
+
["/agent", "Select Codex or Pi"],
|
|
12
|
+
["/session", "Current thread details"],
|
|
13
|
+
["/sessions", "Browse & switch threads"],
|
|
14
|
+
["/sync", "Sync active sessions from CLI state"],
|
|
15
|
+
["/pinned", "Show pinned threads"],
|
|
16
|
+
["/pin", "Pin current or given thread"],
|
|
17
|
+
["/unpin", "Unpin current or given thread"],
|
|
18
|
+
["/attach", "Bind a session to this topic"],
|
|
19
|
+
["/handback", "Hand session back to CLI"],
|
|
20
|
+
["/abort", "Cancel current operation"],
|
|
21
|
+
["/stop", "Cancel current operation"],
|
|
22
|
+
["/retry", "Resend the last prompt"],
|
|
23
|
+
["/queue", "Show queued prompts with cancel buttons"],
|
|
24
|
+
["/cancel", "Cancel a queued prompt"],
|
|
25
|
+
["/clearqueue", "Clear queued prompts"],
|
|
26
|
+
["/artifacts", "List or resend generated files"],
|
|
27
|
+
["/workspaces", "List allowed workspaces"],
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "๐ค Model",
|
|
32
|
+
commands: [
|
|
33
|
+
["/launch_profiles", "Select launch profile"],
|
|
34
|
+
["/fast", "Toggle fast mode"],
|
|
35
|
+
["/model", "View & change model"],
|
|
36
|
+
["/reasoning", "Set reasoning effort"],
|
|
37
|
+
["/mirror", "Control CLI mirroring"],
|
|
38
|
+
["/notify", "Control completion notifications"],
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: "๐ Auth",
|
|
43
|
+
commands: [
|
|
44
|
+
["/auth", "Check auth status"],
|
|
45
|
+
["/login", "Start authentication"],
|
|
46
|
+
["/logout", "Sign out"],
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: "โน๏ธ Utility",
|
|
51
|
+
commands: [
|
|
52
|
+
["/start", "Welcome & status"],
|
|
53
|
+
["/help", "This reference"],
|
|
54
|
+
["/voice", "Voice transcription status"],
|
|
55
|
+
["/status", "Connector runtime status"],
|
|
56
|
+
["/health", "Connector health report"],
|
|
57
|
+
["/version", "Connector version"],
|
|
58
|
+
["/tasks", "Current turn progress"],
|
|
59
|
+
["/activity", "Thread activity timeline"],
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: "๐ ๏ธ Admin",
|
|
64
|
+
commands: [
|
|
65
|
+
["/logs", "Show connector log tail"],
|
|
66
|
+
["/diagnostics", "Connector diagnostics"],
|
|
67
|
+
["/restart", "Restart connector"],
|
|
68
|
+
["/update", "Pull, build, and restart"],
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
const htmlLines = [];
|
|
73
|
+
const plainLines = [];
|
|
74
|
+
for (const section of sections) {
|
|
75
|
+
htmlLines.push(`<b>${escapeHTML(section.title)}</b>`);
|
|
76
|
+
plainLines.push(section.title);
|
|
77
|
+
for (const [cmd, desc] of section.commands) {
|
|
78
|
+
htmlLines.push(` ${cmd} โ ${escapeHTML(desc)}`);
|
|
79
|
+
plainLines.push(` ${cmd} โ ${desc}`);
|
|
80
|
+
}
|
|
81
|
+
htmlLines.push("");
|
|
82
|
+
plainLines.push("");
|
|
83
|
+
}
|
|
84
|
+
while (htmlLines.at(-1) === "") {
|
|
85
|
+
htmlLines.pop();
|
|
86
|
+
}
|
|
87
|
+
while (plainLines.at(-1) === "") {
|
|
88
|
+
plainLines.pop();
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
html: htmlLines.join("\n"),
|
|
92
|
+
plain: plainLines.join("\n"),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Short /start message for first-time users (no prior interaction in this context).
|
|
97
|
+
*/
|
|
98
|
+
export function renderWelcomeFirstTime(authWarning) {
|
|
99
|
+
const htmlLines = [
|
|
100
|
+
"<b>๐ NordRelay is ready.</b>",
|
|
101
|
+
"",
|
|
102
|
+
"Send a message to start chatting with the selected coding agent.",
|
|
103
|
+
"You can also send voice notes, photos, or documents.",
|
|
104
|
+
"",
|
|
105
|
+
"Type /help for all commands.",
|
|
106
|
+
];
|
|
107
|
+
const plainLines = [
|
|
108
|
+
"๐ NordRelay is ready.",
|
|
109
|
+
"",
|
|
110
|
+
"Send a message to start chatting with the selected coding agent.",
|
|
111
|
+
"You can also send voice notes, photos, or documents.",
|
|
112
|
+
"",
|
|
113
|
+
"Type /help for all commands.",
|
|
114
|
+
];
|
|
115
|
+
if (authWarning) {
|
|
116
|
+
htmlLines.push("", `โ ๏ธ ${escapeHTML(authWarning)}`);
|
|
117
|
+
plainLines.push("", `โ ๏ธ ${authWarning}`);
|
|
118
|
+
}
|
|
119
|
+
return { html: htmlLines.join("\n"), plain: plainLines.join("\n") };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Concise /start message for returning users with session info.
|
|
123
|
+
*/
|
|
124
|
+
export function renderWelcomeReturning(sessionHtml, sessionPlain, isTopicSession, authWarning) {
|
|
125
|
+
const label = isTopicSession
|
|
126
|
+
? "NordRelay (topic session)"
|
|
127
|
+
: "NordRelay";
|
|
128
|
+
const htmlLines = [`<b>๐ ${escapeHTML(label)}</b>`, "", sessionHtml];
|
|
129
|
+
const plainLines = [`๐ ${label}`, "", sessionPlain];
|
|
130
|
+
if (authWarning) {
|
|
131
|
+
htmlLines.push("", `โ ๏ธ ${escapeHTML(authWarning)}`);
|
|
132
|
+
plainLines.push("", `โ ๏ธ ${authWarning}`);
|
|
133
|
+
}
|
|
134
|
+
return { html: htmlLines.join("\n"), plain: plainLines.join("\n") };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Format a session button label for /sessions list.
|
|
138
|
+
* Wider workspace name (12 chars), model tag, short thread snippet.
|
|
139
|
+
*/
|
|
140
|
+
export function formatSessionLabel(options) {
|
|
141
|
+
const prefix = options.isActive ? "โ
" : options.isPinned ? "โญ" : "๐";
|
|
142
|
+
const workspaceName = trimLabel(getWorkspaceShortName(options.workspace), 12) || "(unknown)";
|
|
143
|
+
const title = trimLabel(options.title || "(untitled)", 20) || "(untitled)";
|
|
144
|
+
const time = options.relativeTime;
|
|
145
|
+
let label = `${prefix} ${workspaceName} ยท ${title} ยท ${time}`;
|
|
146
|
+
if (options.model) {
|
|
147
|
+
const shortModel = trimLabel(options.model, 10);
|
|
148
|
+
label += ` ยท ${shortModel}`;
|
|
149
|
+
}
|
|
150
|
+
return label;
|
|
151
|
+
}
|
|
152
|
+
function trimLabel(text, maxLength) {
|
|
153
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
154
|
+
if (singleLine.length <= maxLength) {
|
|
155
|
+
return singleLine;
|
|
156
|
+
}
|
|
157
|
+
return `${singleLine.slice(0, maxLength - 1)}โฆ`;
|
|
158
|
+
}
|
|
159
|
+
function getWorkspaceShortName(workspace) {
|
|
160
|
+
return workspace.split(/[\\/]/).filter(Boolean).pop() ?? workspace;
|
|
161
|
+
}
|