@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.
Files changed (45) hide show
  1. package/.env.example +88 -0
  2. package/Dockerfile +19 -0
  3. package/LICENSE +21 -0
  4. package/README.md +749 -0
  5. package/dist/access-control.js +146 -0
  6. package/dist/agent-factory.js +22 -0
  7. package/dist/agent.js +57 -0
  8. package/dist/artifacts.js +515 -0
  9. package/dist/attachments.js +69 -0
  10. package/dist/bot-preferences.js +146 -0
  11. package/dist/bot-ui.js +161 -0
  12. package/dist/bot.js +4520 -0
  13. package/dist/codex-auth.js +150 -0
  14. package/dist/codex-cli.js +79 -0
  15. package/dist/codex-config.js +50 -0
  16. package/dist/codex-launch.js +109 -0
  17. package/dist/codex-session.js +591 -0
  18. package/dist/codex-state.js +573 -0
  19. package/dist/config.js +385 -0
  20. package/dist/context-key.js +23 -0
  21. package/dist/error-messages.js +73 -0
  22. package/dist/format.js +121 -0
  23. package/dist/index.js +140 -0
  24. package/dist/logger.js +27 -0
  25. package/dist/operations.js +133 -0
  26. package/dist/persistence.js +65 -0
  27. package/dist/pi-cli.js +19 -0
  28. package/dist/pi-rpc.js +158 -0
  29. package/dist/pi-session.js +573 -0
  30. package/dist/pi-state.js +226 -0
  31. package/dist/prompt-store.js +241 -0
  32. package/dist/redaction.js +47 -0
  33. package/dist/session-format.js +191 -0
  34. package/dist/session-registry.js +195 -0
  35. package/dist/telegram-rate-limit.js +136 -0
  36. package/dist/voice.js +373 -0
  37. package/dist/workspace-policy.js +41 -0
  38. package/docker-compose.yml +17 -0
  39. package/launchd/start.sh +8 -0
  40. package/package.json +69 -0
  41. package/plugins/nordrelay/.codex-plugin/plugin.json +48 -0
  42. package/plugins/nordrelay/assets/nordrelay.svg +5 -0
  43. package/plugins/nordrelay/commands/remote.md +33 -0
  44. package/plugins/nordrelay/scripts/nordrelay.mjs +396 -0
  45. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +26 -0
@@ -0,0 +1,226 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export function getDefaultPiSessionDir() {
5
+ return path.join(os.homedir(), ".pi", "agent", "sessions");
6
+ }
7
+ export function resolvePiSessionDir(options = {}) {
8
+ return options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR ?? getDefaultPiSessionDir();
9
+ }
10
+ export function listPiSessions(limit = 20, options = {}) {
11
+ const sessionDir = resolvePiSessionDir(options);
12
+ if (!existsSync(sessionDir)) {
13
+ return [];
14
+ }
15
+ const records = [];
16
+ for (const workspaceDir of safeReadDir(sessionDir)) {
17
+ const workspacePath = path.join(sessionDir, workspaceDir);
18
+ if (!safeStat(workspacePath)?.isDirectory()) {
19
+ continue;
20
+ }
21
+ for (const fileName of safeReadDir(workspacePath)) {
22
+ if (!fileName.endsWith(".jsonl")) {
23
+ continue;
24
+ }
25
+ const sessionPath = path.join(workspacePath, fileName);
26
+ const record = readPiSessionRecord(sessionPath, workspaceDir);
27
+ if (record) {
28
+ records.push(record);
29
+ }
30
+ }
31
+ }
32
+ return records
33
+ .sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())
34
+ .slice(0, limit);
35
+ }
36
+ export function getPiSession(idOrPath, options = {}) {
37
+ const normalized = idOrPath.trim();
38
+ if (!normalized) {
39
+ return null;
40
+ }
41
+ if (existsSync(normalized)) {
42
+ return readPiSessionRecord(normalized, path.basename(path.dirname(normalized)));
43
+ }
44
+ const matches = listPiSessions(500, options).filter((record) => record.id === normalized ||
45
+ record.id.startsWith(normalized) ||
46
+ record.sessionPath === normalized ||
47
+ path.basename(record.sessionPath, ".jsonl") === normalized ||
48
+ path.basename(record.sessionPath, ".jsonl").endsWith(`_${normalized}`));
49
+ return matches[0] ?? null;
50
+ }
51
+ export function listPiWorkspaces(options = {}) {
52
+ const workspaces = new Set();
53
+ for (const record of listPiSessions(500, options)) {
54
+ if (record.cwd) {
55
+ workspaces.add(record.cwd);
56
+ }
57
+ }
58
+ return [...workspaces].sort((left, right) => left.localeCompare(right));
59
+ }
60
+ export function readPiSessionRecord(sessionPath, workspaceSlug) {
61
+ try {
62
+ const fileStat = statSync(sessionPath);
63
+ const lines = readFileSync(sessionPath, "utf8")
64
+ .split(/\r?\n/)
65
+ .filter(Boolean);
66
+ const fallbackDate = parsePiSessionDateFromFilename(path.basename(sessionPath)) ?? fileStat.mtime;
67
+ let id = parsePiSessionIdFromFilename(path.basename(sessionPath));
68
+ let createdAt = fallbackDate;
69
+ let updatedAt = fileStat.mtime;
70
+ let cwd = workspaceSlug ? decodePiWorkspaceSlug(workspaceSlug) : path.dirname(sessionPath);
71
+ let model = null;
72
+ let reasoningEffort = null;
73
+ let firstUserMessage = null;
74
+ let lastAssistantText = null;
75
+ let title = null;
76
+ let messageCount = 0;
77
+ for (const line of lines) {
78
+ const entry = safeJsonParse(line);
79
+ if (!entry) {
80
+ continue;
81
+ }
82
+ if (entry.type === "session") {
83
+ id = stringValue(entry.id) ?? id;
84
+ cwd = stringValue(entry.cwd) ?? cwd;
85
+ createdAt = dateValue(entry.timestamp) ?? createdAt;
86
+ }
87
+ else if (entry.type === "model_change") {
88
+ const provider = stringValue(entry.provider);
89
+ const modelId = stringValue(entry.modelId) ?? stringValue(entry.model);
90
+ model = modelId ? (provider ? `${provider}/${modelId}` : modelId) : model;
91
+ }
92
+ else if (entry.type === "thinking_level_change") {
93
+ reasoningEffort = stringValue(entry.thinkingLevel) ?? reasoningEffort;
94
+ }
95
+ const entryTimestamp = dateValue(entry.timestamp);
96
+ if (entryTimestamp && entryTimestamp > updatedAt) {
97
+ updatedAt = entryTimestamp;
98
+ }
99
+ const message = objectValue(entry.message);
100
+ if (message) {
101
+ messageCount += 1;
102
+ const role = stringValue(message.role);
103
+ if (role === "user" && !firstUserMessage) {
104
+ firstUserMessage = extractMessageText(message);
105
+ }
106
+ else if (role === "assistant") {
107
+ const assistantText = extractMessageText(message);
108
+ if (assistantText) {
109
+ lastAssistantText = assistantText;
110
+ }
111
+ model = stringValue(message.model) ?? model;
112
+ }
113
+ }
114
+ title = stringValue(entry.sessionName) ?? stringValue(entry.name) ?? title;
115
+ }
116
+ return {
117
+ id,
118
+ title: title ?? summarizeTitle(firstUserMessage ?? lastAssistantText),
119
+ cwd,
120
+ model,
121
+ reasoningEffort,
122
+ createdAt,
123
+ updatedAt,
124
+ firstUserMessage,
125
+ agentId: "pi",
126
+ sessionPath,
127
+ messageCount,
128
+ };
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ function safeReadDir(directory) {
135
+ try {
136
+ return readdirSync(directory);
137
+ }
138
+ catch {
139
+ return [];
140
+ }
141
+ }
142
+ function safeStat(targetPath) {
143
+ try {
144
+ return statSync(targetPath);
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ function safeJsonParse(line) {
151
+ try {
152
+ const parsed = JSON.parse(line);
153
+ return objectValue(parsed);
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ }
159
+ function objectValue(value) {
160
+ return typeof value === "object" && value !== null ? value : null;
161
+ }
162
+ function stringValue(value) {
163
+ return typeof value === "string" && value.trim() ? value : null;
164
+ }
165
+ function dateValue(value) {
166
+ if (typeof value === "number" && Number.isFinite(value)) {
167
+ return new Date(value);
168
+ }
169
+ if (typeof value === "string" && value.trim()) {
170
+ const timestamp = Date.parse(value);
171
+ return Number.isNaN(timestamp) ? null : new Date(timestamp);
172
+ }
173
+ return null;
174
+ }
175
+ function parsePiSessionIdFromFilename(fileName) {
176
+ const withoutExt = fileName.replace(/\.jsonl$/i, "");
177
+ const uuidMatch = withoutExt.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
178
+ if (uuidMatch) {
179
+ return uuidMatch[1];
180
+ }
181
+ const underscoreIndex = withoutExt.lastIndexOf("_");
182
+ return underscoreIndex === -1 ? withoutExt : withoutExt.slice(underscoreIndex + 1);
183
+ }
184
+ function parsePiSessionDateFromFilename(fileName) {
185
+ const match = fileName.match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)_/);
186
+ if (!match) {
187
+ return null;
188
+ }
189
+ const normalized = match[1]
190
+ .replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
191
+ const timestamp = Date.parse(normalized);
192
+ return Number.isNaN(timestamp) ? null : new Date(timestamp);
193
+ }
194
+ function decodePiWorkspaceSlug(slug) {
195
+ const normalized = slug.replace(/^--/, "").replace(/--$/, "");
196
+ if (!normalized) {
197
+ return "/";
198
+ }
199
+ return `/${normalized.replace(/-/g, "/")}`;
200
+ }
201
+ function extractMessageText(message) {
202
+ const content = message.content;
203
+ if (typeof content === "string") {
204
+ return content.trim() || null;
205
+ }
206
+ if (!Array.isArray(content)) {
207
+ return null;
208
+ }
209
+ const parts = content
210
+ .map((part) => {
211
+ const block = objectValue(part);
212
+ if (!block) {
213
+ return "";
214
+ }
215
+ return stringValue(block.text) ?? stringValue(block.thinking) ?? "";
216
+ })
217
+ .filter(Boolean);
218
+ return parts.join("\n").trim() || null;
219
+ }
220
+ function summarizeTitle(text) {
221
+ if (!text) {
222
+ return null;
223
+ }
224
+ const normalized = text.replace(/\s+/g, " ").trim();
225
+ return normalized.length <= 60 ? normalized : `${normalized.slice(0, 57)}...`;
226
+ }
@@ -0,0 +1,241 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { readJsonFileWithBackup, writeJsonFileAtomic } from "./persistence.js";
5
+ export class PromptStore {
6
+ persistPath;
7
+ lastPrompts = new Map();
8
+ queues = new Map();
9
+ pausedContexts = new Set();
10
+ constructor(workspace) {
11
+ this.persistPath = path.join(workspace, ".nordrelay", "prompts.json");
12
+ this.load();
13
+ }
14
+ setLastPrompt(contextKey, prompt) {
15
+ this.lastPrompts.set(contextKey, prompt);
16
+ this.persist();
17
+ }
18
+ getLastPrompt(contextKey) {
19
+ return this.lastPrompts.get(contextKey);
20
+ }
21
+ enqueue(contextKey, prompt) {
22
+ const item = {
23
+ ...prompt,
24
+ id: createQueueId(),
25
+ contextKey,
26
+ createdAt: Date.now(),
27
+ };
28
+ const queue = this.queues.get(contextKey) ?? [];
29
+ queue.push(item);
30
+ this.queues.set(contextKey, queue);
31
+ this.persist();
32
+ return item;
33
+ }
34
+ enqueueFront(contextKey, prompt) {
35
+ const queue = this.queues.get(contextKey) ?? [];
36
+ queue.unshift(prompt);
37
+ this.queues.set(contextKey, queue);
38
+ this.persist();
39
+ }
40
+ dequeue(contextKey) {
41
+ const queue = this.queues.get(contextKey);
42
+ const item = queue?.shift();
43
+ if (!queue || queue.length === 0) {
44
+ this.queues.delete(contextKey);
45
+ }
46
+ if (item) {
47
+ item.attempts = (item.attempts ?? 0) + 1;
48
+ item.updatedAt = Date.now();
49
+ }
50
+ this.persist();
51
+ return item;
52
+ }
53
+ list(contextKey) {
54
+ return [...(this.queues.get(contextKey) ?? [])];
55
+ }
56
+ listContextKeys() {
57
+ return [...new Set([...this.queues.keys(), ...this.pausedContexts])];
58
+ }
59
+ remove(contextKey, id) {
60
+ const queue = this.queues.get(contextKey);
61
+ if (!queue) {
62
+ return undefined;
63
+ }
64
+ const index = queue.findIndex((item) => item.id === id);
65
+ if (index === -1) {
66
+ return undefined;
67
+ }
68
+ const [removed] = queue.splice(index, 1);
69
+ if (queue.length === 0) {
70
+ this.queues.delete(contextKey);
71
+ }
72
+ this.persist();
73
+ return removed;
74
+ }
75
+ moveToTop(contextKey, id) {
76
+ const queue = this.queues.get(contextKey);
77
+ if (!queue) {
78
+ return undefined;
79
+ }
80
+ const index = queue.findIndex((item) => item.id === id);
81
+ if (index === -1) {
82
+ return undefined;
83
+ }
84
+ const [item] = queue.splice(index, 1);
85
+ queue.unshift(item);
86
+ this.persist();
87
+ return item;
88
+ }
89
+ moveUp(contextKey, id) {
90
+ const queue = this.queues.get(contextKey);
91
+ if (!queue) {
92
+ return undefined;
93
+ }
94
+ const index = queue.findIndex((item) => item.id === id);
95
+ if (index <= 0) {
96
+ return queue[index];
97
+ }
98
+ const [item] = queue.splice(index, 1);
99
+ queue.splice(index - 1, 0, item);
100
+ item.updatedAt = Date.now();
101
+ this.persist();
102
+ return item;
103
+ }
104
+ moveDown(contextKey, id) {
105
+ const queue = this.queues.get(contextKey);
106
+ if (!queue) {
107
+ return undefined;
108
+ }
109
+ const index = queue.findIndex((item) => item.id === id);
110
+ if (index === -1) {
111
+ return undefined;
112
+ }
113
+ if (index >= queue.length - 1) {
114
+ return queue[index];
115
+ }
116
+ const [item] = queue.splice(index, 1);
117
+ queue.splice(index + 1, 0, item);
118
+ item.updatedAt = Date.now();
119
+ this.persist();
120
+ return item;
121
+ }
122
+ markFailed(contextKey, item, error) {
123
+ item.lastError = error;
124
+ item.updatedAt = Date.now();
125
+ this.enqueueFront(contextKey, item);
126
+ }
127
+ clear(contextKey) {
128
+ const count = this.queues.get(contextKey)?.length ?? 0;
129
+ this.queues.delete(contextKey);
130
+ this.persist();
131
+ return count;
132
+ }
133
+ pause(contextKey) {
134
+ this.pausedContexts.add(contextKey);
135
+ this.persist();
136
+ }
137
+ resume(contextKey) {
138
+ this.pausedContexts.delete(contextKey);
139
+ this.persist();
140
+ }
141
+ isPaused(contextKey) {
142
+ return this.pausedContexts.has(contextKey);
143
+ }
144
+ persist() {
145
+ try {
146
+ mkdirSync(path.dirname(this.persistPath), { recursive: true });
147
+ const payload = {
148
+ lastPrompts: Object.fromEntries(this.lastPrompts.entries()),
149
+ queues: Object.fromEntries(this.queues.entries()),
150
+ pausedContexts: [...this.pausedContexts],
151
+ };
152
+ writeJsonFileAtomic(this.persistPath, payload);
153
+ }
154
+ catch (error) {
155
+ console.warn("Failed to persist prompt store:", error instanceof Error ? error.message : String(error));
156
+ }
157
+ }
158
+ load() {
159
+ try {
160
+ const payload = readJsonFileWithBackup(this.persistPath).value;
161
+ if (!payload) {
162
+ return;
163
+ }
164
+ for (const [contextKey, prompt] of Object.entries(payload.lastPrompts ?? {})) {
165
+ if (isPromptEnvelope(prompt)) {
166
+ this.lastPrompts.set(contextKey, prompt);
167
+ }
168
+ }
169
+ for (const [contextKey, queue] of Object.entries(payload.queues ?? {})) {
170
+ if (Array.isArray(queue)) {
171
+ this.queues.set(contextKey, queue.filter(isQueuedPrompt));
172
+ }
173
+ }
174
+ if (Array.isArray(payload.pausedContexts)) {
175
+ this.pausedContexts = new Set(payload.pausedContexts.filter((contextKey) => typeof contextKey === "string"));
176
+ }
177
+ }
178
+ catch (error) {
179
+ console.warn("Failed to load prompt store:", error instanceof Error ? error.message : String(error));
180
+ }
181
+ }
182
+ }
183
+ export function describePromptInput(input) {
184
+ if (typeof input === "string") {
185
+ return trimDescription(input);
186
+ }
187
+ const parts = [];
188
+ if (input.text) {
189
+ parts.push(trimDescription(input.text));
190
+ }
191
+ if (input.imagePaths?.length) {
192
+ parts.push(`${input.imagePaths.length} image${input.imagePaths.length === 1 ? "" : "s"}`);
193
+ }
194
+ if (input.stagedFileInstructions) {
195
+ parts.push("staged file input");
196
+ }
197
+ return parts.join(" · ") || "prompt";
198
+ }
199
+ export function toPromptEnvelope(input, artifactOutDir) {
200
+ return {
201
+ input,
202
+ artifactOutDir,
203
+ description: describePromptInput(input),
204
+ };
205
+ }
206
+ function createQueueId() {
207
+ return randomUUID().replace(/-/g, "").slice(0, 8);
208
+ }
209
+ function isPromptEnvelope(value) {
210
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
211
+ return false;
212
+ }
213
+ const candidate = value;
214
+ return isCodexPromptInput(candidate.input) && typeof candidate.description === "string";
215
+ }
216
+ function isQueuedPrompt(value) {
217
+ return isPromptEnvelope(value) &&
218
+ typeof value.id === "string" &&
219
+ typeof value.contextKey === "string" &&
220
+ typeof value.createdAt === "number" &&
221
+ (value.updatedAt === undefined || typeof value.updatedAt === "number") &&
222
+ (value.attempts === undefined || typeof value.attempts === "number") &&
223
+ (value.lastError === undefined || typeof value.lastError === "string");
224
+ }
225
+ function isCodexPromptInput(value) {
226
+ if (typeof value === "string") {
227
+ return true;
228
+ }
229
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
230
+ return false;
231
+ }
232
+ const candidate = value;
233
+ return ((candidate.text === undefined || typeof candidate.text === "string") &&
234
+ (candidate.stagedFileInstructions === undefined || typeof candidate.stagedFileInstructions === "string") &&
235
+ (candidate.imagePaths === undefined ||
236
+ (Array.isArray(candidate.imagePaths) && candidate.imagePaths.every((item) => typeof item === "string"))));
237
+ }
238
+ function trimDescription(text) {
239
+ const singleLine = text.replace(/\s+/g, " ").trim();
240
+ return singleLine.length <= 80 ? singleLine : `${singleLine.slice(0, 79)}…`;
241
+ }
@@ -0,0 +1,47 @@
1
+ const DEFAULT_SECRET_PATTERNS = [
2
+ /\b\d{6,}:[A-Za-z0-9_-]{24,}\b/g,
3
+ /\bsk-[A-Za-z0-9_-]{20,}\b/g,
4
+ /\b(?:OPENAI|CODEX|TELEGRAM|ANTHROPIC|GITHUB|GITLAB|NPM)_[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)\s*=\s*[^\s"'`]+/gi,
5
+ /\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)\s*[:=]\s*[^\s"'`]+/gi,
6
+ ];
7
+ let configuredPatterns = [];
8
+ export function configureRedaction(rawPatterns) {
9
+ configuredPatterns = rawPatterns
10
+ .map((pattern) => {
11
+ try {
12
+ return new RegExp(pattern, "gi");
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ })
18
+ .filter((pattern) => Boolean(pattern));
19
+ }
20
+ export function redactText(text) {
21
+ let redacted = text;
22
+ for (const pattern of [...DEFAULT_SECRET_PATTERNS, ...configuredPatterns]) {
23
+ redacted = redacted.replace(pattern, (match) => redactMatch(match));
24
+ }
25
+ return redacted;
26
+ }
27
+ export function redactUnknown(value) {
28
+ if (value instanceof Error) {
29
+ return `${value.name}: ${redactText(value.message)}`;
30
+ }
31
+ if (typeof value === "string") {
32
+ return redactText(value);
33
+ }
34
+ try {
35
+ return redactText(JSON.stringify(value));
36
+ }
37
+ catch {
38
+ return redactText(String(value));
39
+ }
40
+ }
41
+ function redactMatch(match) {
42
+ const separator = match.match(/^([^:=]+[:=]\s*)/);
43
+ if (separator?.[1]) {
44
+ return `${separator[1]}[REDACTED]`;
45
+ }
46
+ return "[REDACTED]";
47
+ }