@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,573 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { isCodexApprovalPolicy, isCodexSandboxMode, } from "./codex-launch.js";
4
+ export const FALLBACK_MODELS = [
5
+ { slug: "gpt-5.5", displayName: "GPT-5.5" },
6
+ { slug: "gpt-5.4", displayName: "GPT-5.4" },
7
+ { slug: "gpt-5.4-mini", displayName: "GPT-5.4-Mini" },
8
+ { slug: "gpt-5", displayName: "GPT-5" },
9
+ { slug: "o4-mini", displayName: "o4-mini" },
10
+ { slug: "o3", displayName: "o3" },
11
+ { slug: "o3-mini", displayName: "o3-mini" },
12
+ { slug: "gpt-4o", displayName: "GPT-4o" },
13
+ ];
14
+ const betterSqlite3Module = await import("better-sqlite3").catch(() => null);
15
+ const BetterSqlite3 = (betterSqlite3Module?.default ??
16
+ betterSqlite3Module);
17
+ export function findLatestDatabase() {
18
+ const codexDir = getCodexDir();
19
+ if (!codexDir || !existsSync(codexDir)) {
20
+ return null;
21
+ }
22
+ try {
23
+ const candidates = readdirSync(codexDir)
24
+ .filter((file) => /^state_.*\.sqlite$/i.test(file))
25
+ .map((file) => {
26
+ const fullPath = path.join(codexDir, file);
27
+ return {
28
+ path: fullPath,
29
+ modifiedAtMs: statSync(fullPath).mtimeMs,
30
+ };
31
+ })
32
+ .sort((left, right) => right.modifiedAtMs - left.modifiedAtMs);
33
+ return candidates[0]?.path ?? null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function listThreads(limit = 20) {
40
+ return withDatabase((db) => {
41
+ const query = db.prepare(`
42
+ SELECT id, title, cwd, model, reasoning_effort, sandbox_policy, approval_mode, created_at, updated_at, first_user_message
43
+ FROM threads
44
+ WHERE (archived = 0 OR archived IS NULL)
45
+ ORDER BY updated_at DESC
46
+ LIMIT ?
47
+ `);
48
+ const rows = query.all(limit);
49
+ return rows.map(mapThreadRow);
50
+ }) ?? [];
51
+ }
52
+ export function getThread(id) {
53
+ return (withDatabase((db) => {
54
+ const query = db.prepare(`
55
+ SELECT id, title, cwd, model, reasoning_effort, sandbox_policy, approval_mode, created_at, updated_at, first_user_message
56
+ FROM threads
57
+ WHERE archived = 0 AND id = ?
58
+ LIMIT 1
59
+ `);
60
+ const row = query.get(id);
61
+ return row ? mapThreadRow(row) : null;
62
+ }) ?? null);
63
+ }
64
+ export function getThreadUsage(id) {
65
+ const rolloutPath = getThreadRolloutPath(id);
66
+ if (!rolloutPath || !existsSync(rolloutPath)) {
67
+ return null;
68
+ }
69
+ try {
70
+ return parseUsageFromRollout(readFileSync(rolloutPath, "utf8"));
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ export function getThreadActivity(id, options = {}) {
77
+ const rolloutPath = getThreadRolloutPath(id);
78
+ if (!rolloutPath || !existsSync(rolloutPath)) {
79
+ return null;
80
+ }
81
+ try {
82
+ const parsed = parseActivityFromRollout(id, rolloutPath, readFileSync(rolloutPath, "utf8"));
83
+ const fileModifiedAtMs = statSync(rolloutPath).mtimeMs;
84
+ const updatedAtMs = Math.max(parsed.updatedAt?.getTime() ?? 0, fileModifiedAtMs);
85
+ const updatedAt = updatedAtMs > 0 ? new Date(updatedAtMs) : parsed.updatedAt;
86
+ const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
87
+ const nowMs = options.nowMs ?? Date.now();
88
+ const stale = Boolean(parsed.active &&
89
+ updatedAt &&
90
+ staleAfterMs > 0 &&
91
+ nowMs - updatedAt.getTime() > staleAfterMs);
92
+ return {
93
+ ...parsed,
94
+ updatedAt,
95
+ stale,
96
+ active: parsed.active && !stale,
97
+ };
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ export function getThreadRolloutSnapshot(id, options = {}) {
104
+ const rolloutPath = getThreadRolloutPath(id);
105
+ if (!rolloutPath || !existsSync(rolloutPath)) {
106
+ return null;
107
+ }
108
+ try {
109
+ const parsed = parseRolloutSnapshot(id, rolloutPath, readFileSync(rolloutPath, "utf8"));
110
+ return finalizeRolloutSnapshot(parsed, statSync(rolloutPath).mtimeMs, options);
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ export function getThreadActivityLog(id, limit = 20) {
117
+ const snapshot = getThreadRolloutSnapshot(id, { maxEvents: limit });
118
+ return snapshot?.events ?? [];
119
+ }
120
+ export function listWorkspaces() {
121
+ return (withDatabase((db) => {
122
+ const query = db.prepare(`
123
+ SELECT DISTINCT cwd
124
+ FROM threads
125
+ WHERE (archived = 0 OR archived IS NULL) AND cwd IS NOT NULL AND cwd != ''
126
+ ORDER BY cwd ASC
127
+ `);
128
+ const rows = query.all();
129
+ return rows
130
+ .map((row) => (typeof row.cwd === "string" ? row.cwd : ""))
131
+ .filter(Boolean);
132
+ }) ?? []);
133
+ }
134
+ export function listModels() {
135
+ const modelsPath = getModelsCachePath();
136
+ if (!modelsPath || !existsSync(modelsPath)) {
137
+ return FALLBACK_MODELS;
138
+ }
139
+ try {
140
+ const payload = JSON.parse(readFileSync(modelsPath, "utf8"));
141
+ const models = (payload.models ?? [])
142
+ .filter((model) => model && typeof model === "object")
143
+ .filter((model) => model.visibility !== "hidden")
144
+ .map((model) => ({
145
+ slug: typeof model.slug === "string" ? model.slug : "",
146
+ displayName: typeof model.display_name === "string" ? model.display_name : "",
147
+ }))
148
+ .filter((model) => model.slug && model.displayName);
149
+ return models.length > 0 ? models : FALLBACK_MODELS;
150
+ }
151
+ catch {
152
+ return FALLBACK_MODELS;
153
+ }
154
+ }
155
+ export function getThreadRolloutPath(id) {
156
+ return (withDatabase((db) => {
157
+ const query = db.prepare(`
158
+ SELECT rollout_path
159
+ FROM threads
160
+ WHERE archived = 0 AND id = ?
161
+ LIMIT 1
162
+ `);
163
+ const row = query.get(id);
164
+ return typeof row?.rollout_path === "string" && row.rollout_path.trim()
165
+ ? row.rollout_path
166
+ : null;
167
+ }) ?? null);
168
+ }
169
+ function parseUsageFromRollout(contents) {
170
+ let contextWindow = null;
171
+ let contextUsedPercent = null;
172
+ let lastTokenUsage = null;
173
+ let totalTokenUsage = null;
174
+ let rateLimits = null;
175
+ let updatedAt = null;
176
+ for (const line of contents.split(/\r?\n/)) {
177
+ if (!line.includes('"token_count"')) {
178
+ continue;
179
+ }
180
+ let event;
181
+ try {
182
+ event = JSON.parse(line);
183
+ }
184
+ catch {
185
+ continue;
186
+ }
187
+ const payload = readObject(readObject(event)?.payload);
188
+ if (payload?.type !== "token_count") {
189
+ continue;
190
+ }
191
+ const timestamp = readString(readObject(event)?.timestamp);
192
+ if (timestamp) {
193
+ const parsedTimestamp = new Date(timestamp);
194
+ if (!Number.isNaN(parsedTimestamp.getTime())) {
195
+ updatedAt = parsedTimestamp;
196
+ }
197
+ }
198
+ const info = readObject(payload.info);
199
+ const parsedTotal = parseTokenUsage(readObject(info?.total_token_usage));
200
+ const parsedLast = parseTokenUsage(readObject(info?.last_token_usage));
201
+ const parsedContextWindow = readNumber(info?.model_context_window);
202
+ if (parsedTotal) {
203
+ totalTokenUsage = parsedTotal;
204
+ }
205
+ if (parsedLast) {
206
+ lastTokenUsage = parsedLast;
207
+ }
208
+ if (parsedContextWindow !== null && parsedContextWindow > 0) {
209
+ contextWindow = parsedContextWindow;
210
+ }
211
+ if (lastTokenUsage && contextWindow) {
212
+ contextUsedPercent = Math.min(100, (lastTokenUsage.totalTokens / contextWindow) * 100);
213
+ }
214
+ const parsedRateLimits = parseRateLimits(readObject(payload.rate_limits));
215
+ if (parsedRateLimits) {
216
+ rateLimits = parsedRateLimits;
217
+ }
218
+ }
219
+ if (!lastTokenUsage && !totalTokenUsage && !rateLimits) {
220
+ return null;
221
+ }
222
+ return {
223
+ contextWindow,
224
+ contextUsedPercent,
225
+ lastTokenUsage,
226
+ totalTokenUsage,
227
+ rateLimits,
228
+ updatedAt,
229
+ };
230
+ }
231
+ function parseActivityFromRollout(threadId, rolloutPath, contents) {
232
+ return parseRolloutSnapshot(threadId, rolloutPath, contents).activity;
233
+ }
234
+ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
235
+ let activeTurnId = null;
236
+ let startedAt = null;
237
+ let updatedAt = null;
238
+ let latestAgentMessage = null;
239
+ let latestUserMessage = null;
240
+ let latestToolName = null;
241
+ const events = [];
242
+ const lines = contents.split(/\r?\n/);
243
+ for (const [index, line] of lines.entries()) {
244
+ if (!line.trim()) {
245
+ continue;
246
+ }
247
+ if (!line.includes('"task_') &&
248
+ !line.includes('"turn_') &&
249
+ !line.includes('"user_message"') &&
250
+ !line.includes('"agent_message"') &&
251
+ !line.includes('"function_call"') &&
252
+ !activeTurnId) {
253
+ continue;
254
+ }
255
+ let event;
256
+ try {
257
+ event = JSON.parse(line);
258
+ }
259
+ catch {
260
+ continue;
261
+ }
262
+ const eventObject = readObject(event);
263
+ const payload = readObject(eventObject?.payload);
264
+ const eventTimestamp = parseTimestamp(readString(eventObject?.timestamp));
265
+ const lineNumber = index + 1;
266
+ if (activeTurnId && eventTimestamp) {
267
+ updatedAt = eventTimestamp;
268
+ }
269
+ const type = readString(payload?.type);
270
+ if (!type) {
271
+ continue;
272
+ }
273
+ if (type === "task_started") {
274
+ activeTurnId = readString(payload?.turn_id);
275
+ startedAt = parseUnixSeconds(readNumber(payload?.started_at)) ?? eventTimestamp;
276
+ updatedAt = eventTimestamp ?? startedAt;
277
+ events.push({
278
+ lineNumber,
279
+ kind: "task",
280
+ timestamp: eventTimestamp,
281
+ type,
282
+ turnId: activeTurnId,
283
+ status: "started",
284
+ text: null,
285
+ toolName: null,
286
+ phase: null,
287
+ });
288
+ continue;
289
+ }
290
+ if (isTaskTerminalEvent(type)) {
291
+ const turnId = readString(payload?.turn_id);
292
+ events.push({
293
+ lineNumber,
294
+ kind: "task",
295
+ timestamp: eventTimestamp,
296
+ type,
297
+ turnId,
298
+ status: terminalStatusForEvent(type),
299
+ text: readString(payload?.last_agent_message),
300
+ toolName: null,
301
+ phase: null,
302
+ });
303
+ if (!activeTurnId || !turnId || turnId === activeTurnId) {
304
+ activeTurnId = null;
305
+ startedAt = null;
306
+ updatedAt = eventTimestamp ?? updatedAt;
307
+ }
308
+ continue;
309
+ }
310
+ if (type === "user_message") {
311
+ latestUserMessage = readString(payload?.message);
312
+ events.push({
313
+ lineNumber,
314
+ kind: "user",
315
+ timestamp: eventTimestamp,
316
+ type,
317
+ turnId: activeTurnId,
318
+ status: null,
319
+ text: latestUserMessage,
320
+ toolName: null,
321
+ phase: null,
322
+ });
323
+ continue;
324
+ }
325
+ if (type === "agent_message") {
326
+ latestAgentMessage = readString(payload?.message);
327
+ events.push({
328
+ lineNumber,
329
+ kind: "agent",
330
+ timestamp: eventTimestamp,
331
+ type,
332
+ turnId: activeTurnId,
333
+ status: null,
334
+ text: latestAgentMessage,
335
+ toolName: null,
336
+ phase: readString(payload?.phase),
337
+ });
338
+ continue;
339
+ }
340
+ if (type === "function_call") {
341
+ latestToolName = readString(payload?.name);
342
+ events.push({
343
+ lineNumber,
344
+ kind: "tool",
345
+ timestamp: eventTimestamp,
346
+ type,
347
+ turnId: activeTurnId,
348
+ status: "started",
349
+ text: null,
350
+ toolName: latestToolName,
351
+ phase: null,
352
+ });
353
+ continue;
354
+ }
355
+ if (type === "function_call_output") {
356
+ events.push({
357
+ lineNumber,
358
+ kind: "tool",
359
+ timestamp: eventTimestamp,
360
+ type,
361
+ turnId: activeTurnId,
362
+ status: "finished",
363
+ text: null,
364
+ toolName: null,
365
+ phase: null,
366
+ });
367
+ }
368
+ }
369
+ return {
370
+ threadId,
371
+ rolloutPath,
372
+ lineCount: lines.filter((line) => line.trim()).length,
373
+ activity: {
374
+ threadId,
375
+ rolloutPath,
376
+ active: Boolean(activeTurnId),
377
+ stale: false,
378
+ turnId: activeTurnId,
379
+ startedAt,
380
+ updatedAt,
381
+ },
382
+ events,
383
+ latestAgentMessage,
384
+ latestUserMessage,
385
+ latestToolName,
386
+ };
387
+ }
388
+ function finalizeRolloutSnapshot(snapshot, fileModifiedAtMs, options) {
389
+ const updatedAtMs = Math.max(snapshot.activity.updatedAt?.getTime() ?? 0, fileModifiedAtMs);
390
+ const updatedAt = updatedAtMs > 0 ? new Date(updatedAtMs) : snapshot.activity.updatedAt;
391
+ const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
392
+ const nowMs = options.nowMs ?? Date.now();
393
+ const stale = Boolean(snapshot.activity.active &&
394
+ updatedAt &&
395
+ staleAfterMs > 0 &&
396
+ nowMs - updatedAt.getTime() > staleAfterMs);
397
+ const afterLine = options.afterLine ?? 0;
398
+ const maxEvents = options.maxEvents ?? Number.POSITIVE_INFINITY;
399
+ const filteredEvents = snapshot.events.filter((event) => event.lineNumber > afterLine);
400
+ const events = maxEvents <= 0 ? [] : filteredEvents.slice(-maxEvents);
401
+ return {
402
+ ...snapshot,
403
+ activity: {
404
+ ...snapshot.activity,
405
+ updatedAt,
406
+ stale,
407
+ active: snapshot.activity.active && !stale,
408
+ },
409
+ events,
410
+ };
411
+ }
412
+ function isTaskTerminalEvent(type) {
413
+ return [
414
+ "task_complete",
415
+ "task_failed",
416
+ "task_error",
417
+ "turn_aborted",
418
+ "turn_complete",
419
+ "turn_failed",
420
+ "turn_error",
421
+ ].includes(type);
422
+ }
423
+ function terminalStatusForEvent(type) {
424
+ if (type.includes("abort")) {
425
+ return "aborted";
426
+ }
427
+ if (type.includes("fail") || type.includes("error")) {
428
+ return "failed";
429
+ }
430
+ return "completed";
431
+ }
432
+ function parseTokenUsage(value) {
433
+ if (!value) {
434
+ return null;
435
+ }
436
+ const inputTokens = readNumber(value.input_tokens);
437
+ const cachedInputTokens = readNumber(value.cached_input_tokens);
438
+ const outputTokens = readNumber(value.output_tokens);
439
+ const reasoningOutputTokens = readNumber(value.reasoning_output_tokens);
440
+ const totalTokens = readNumber(value.total_tokens);
441
+ if (inputTokens === null ||
442
+ cachedInputTokens === null ||
443
+ outputTokens === null ||
444
+ reasoningOutputTokens === null ||
445
+ totalTokens === null) {
446
+ return null;
447
+ }
448
+ return {
449
+ inputTokens,
450
+ cachedInputTokens,
451
+ outputTokens,
452
+ reasoningOutputTokens,
453
+ totalTokens,
454
+ };
455
+ }
456
+ function parseRateLimits(value) {
457
+ if (!value) {
458
+ return null;
459
+ }
460
+ return {
461
+ limitId: readString(value.limit_id),
462
+ limitName: readString(value.limit_name),
463
+ planType: readString(value.plan_type),
464
+ primary: parseRateLimitWindow(readObject(value.primary)),
465
+ secondary: parseRateLimitWindow(readObject(value.secondary)),
466
+ };
467
+ }
468
+ function parseRateLimitWindow(value) {
469
+ if (!value) {
470
+ return null;
471
+ }
472
+ const usedPercent = readNumber(value.used_percent);
473
+ const windowMinutes = readNumber(value.window_minutes);
474
+ if (usedPercent === null || windowMinutes === null) {
475
+ return null;
476
+ }
477
+ const resetSeconds = readNumber(value.resets_at);
478
+ return {
479
+ usedPercent,
480
+ remainingPercent: Math.max(0, 100 - usedPercent),
481
+ windowMinutes,
482
+ resetsAt: resetSeconds === null ? null : new Date(resetSeconds * 1000),
483
+ };
484
+ }
485
+ function mapThreadRow(row) {
486
+ return {
487
+ id: typeof row.id === "string" ? row.id : String(row.id ?? ""),
488
+ title: typeof row.title === "string" ? row.title : "",
489
+ cwd: typeof row.cwd === "string" ? row.cwd : "",
490
+ model: typeof row.model === "string" ? row.model : null,
491
+ reasoningEffort: typeof row.reasoning_effort === "string" ? row.reasoning_effort : null,
492
+ sandboxMode: parseSandboxPolicy(row.sandbox_policy),
493
+ approvalPolicy: parseApprovalMode(row.approval_mode),
494
+ createdAt: fromUnixSeconds(row.created_at),
495
+ updatedAt: fromUnixSeconds(row.updated_at),
496
+ firstUserMessage: typeof row.first_user_message === "string" ? row.first_user_message : "",
497
+ };
498
+ }
499
+ function parseApprovalMode(value) {
500
+ return typeof value === "string" && isCodexApprovalPolicy(value) ? value : null;
501
+ }
502
+ function parseSandboxPolicy(value) {
503
+ if (typeof value !== "string" || !value.trim()) {
504
+ return null;
505
+ }
506
+ if (isCodexSandboxMode(value)) {
507
+ return value;
508
+ }
509
+ try {
510
+ const parsed = JSON.parse(value);
511
+ return typeof parsed.type === "string" && isCodexSandboxMode(parsed.type) ? parsed.type : null;
512
+ }
513
+ catch {
514
+ return null;
515
+ }
516
+ }
517
+ function fromUnixSeconds(value) {
518
+ return typeof value === "number" ? new Date(value * 1000) : new Date(0);
519
+ }
520
+ function parseUnixSeconds(value) {
521
+ return value === null ? null : new Date(value * 1000);
522
+ }
523
+ function parseTimestamp(value) {
524
+ if (!value) {
525
+ return null;
526
+ }
527
+ const parsed = new Date(value);
528
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
529
+ }
530
+ function readObject(value) {
531
+ return value && typeof value === "object" && !Array.isArray(value)
532
+ ? value
533
+ : null;
534
+ }
535
+ function readNumber(value) {
536
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
537
+ }
538
+ function readString(value) {
539
+ return typeof value === "string" ? value : null;
540
+ }
541
+ function withDatabase(fn) {
542
+ if (!BetterSqlite3) {
543
+ return null;
544
+ }
545
+ const databasePath = findLatestDatabase();
546
+ if (!databasePath) {
547
+ return null;
548
+ }
549
+ let db = null;
550
+ try {
551
+ db = new BetterSqlite3(databasePath, { readonly: true, fileMustExist: true });
552
+ return fn(db);
553
+ }
554
+ catch {
555
+ return null;
556
+ }
557
+ finally {
558
+ try {
559
+ db?.close();
560
+ }
561
+ catch {
562
+ // Ignore close failures.
563
+ }
564
+ }
565
+ }
566
+ function getCodexDir() {
567
+ const home = process.env.HOME?.trim();
568
+ return home ? path.join(home, ".codex") : null;
569
+ }
570
+ function getModelsCachePath() {
571
+ const codexDir = getCodexDir();
572
+ return codexDir ? path.join(codexDir, "models_cache.json") : null;
573
+ }