@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,150 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
const CODEX_CLI = "codex";
|
|
3
|
+
const COMMAND_TIMEOUT_MS = 10_000;
|
|
4
|
+
const AUTH_CACHE_TTL_MS = 30_000;
|
|
5
|
+
let cachedAuthStatus;
|
|
6
|
+
/**
|
|
7
|
+
* Check whether Codex is currently authenticated.
|
|
8
|
+
*
|
|
9
|
+
* Priority:
|
|
10
|
+
* 1. If CODEX_API_KEY is set in the environment, report authenticated via API key.
|
|
11
|
+
* 2. Otherwise, shell out to `codex login status` to check CLI auth.
|
|
12
|
+
* 3. If the CLI command fails or is unavailable, report unauthenticated.
|
|
13
|
+
*
|
|
14
|
+
* Results are cached for 30 seconds to avoid per-message CLI invocations.
|
|
15
|
+
*/
|
|
16
|
+
export async function checkAuthStatus(apiKey) {
|
|
17
|
+
if (apiKey) {
|
|
18
|
+
return {
|
|
19
|
+
authenticated: true,
|
|
20
|
+
method: "api-key",
|
|
21
|
+
detail: "Authenticated via CODEX_API_KEY",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (cachedAuthStatus && Date.now() < cachedAuthStatus.expiresAt) {
|
|
25
|
+
return cachedAuthStatus.status;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const { stdout } = await runCodexCommand(["login", "status"]);
|
|
29
|
+
const output = stdout.trim();
|
|
30
|
+
const status = {
|
|
31
|
+
authenticated: true,
|
|
32
|
+
method: "cli",
|
|
33
|
+
detail: output || "Authenticated via Codex CLI",
|
|
34
|
+
};
|
|
35
|
+
cachedAuthStatus = { status, expiresAt: Date.now() + AUTH_CACHE_TTL_MS };
|
|
36
|
+
return status;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const status = parseCommandError(error);
|
|
40
|
+
cachedAuthStatus = { status, expiresAt: Date.now() + AUTH_CACHE_TTL_MS };
|
|
41
|
+
return status;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Clear the cached auth status so the next check hits the CLI.
|
|
46
|
+
*/
|
|
47
|
+
export function clearAuthCache() {
|
|
48
|
+
cachedAuthStatus = undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Attempt to start a login flow via the Codex CLI.
|
|
52
|
+
* Uses --device-auth to get a device code flow suitable for headless/remote hosts.
|
|
53
|
+
*/
|
|
54
|
+
export async function startLogin() {
|
|
55
|
+
clearAuthCache();
|
|
56
|
+
try {
|
|
57
|
+
const { stdout } = await runCodexCommand(["login", "--device-auth"]);
|
|
58
|
+
const output = stdout.trim();
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
message: output || "Login initiated. Check your terminal or browser for the next step.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const detail = extractErrorMessage(error);
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
message: detail || "Login command failed. Try running 'codex auth login' on the host.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Attempt to logout via the Codex CLI.
|
|
74
|
+
*/
|
|
75
|
+
export async function startLogout() {
|
|
76
|
+
clearAuthCache();
|
|
77
|
+
try {
|
|
78
|
+
const { stdout } = await runCodexCommand(["logout"]);
|
|
79
|
+
const output = stdout.trim();
|
|
80
|
+
return {
|
|
81
|
+
success: true,
|
|
82
|
+
message: output || "Logged out successfully.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const detail = extractErrorMessage(error);
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
message: detail || "Logout command failed. Try running 'codex auth logout' on the host.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function runCodexCommand(args) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
execFile(CODEX_CLI, args, {
|
|
96
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
97
|
+
env: { ...process.env },
|
|
98
|
+
maxBuffer: 1024 * 1024,
|
|
99
|
+
}, (error, stdout, stderr) => {
|
|
100
|
+
if (error) {
|
|
101
|
+
// Attach stdout/stderr to the error for richer diagnostics
|
|
102
|
+
const enriched = error;
|
|
103
|
+
enriched.stdout = typeof stdout === "string" ? stdout : "";
|
|
104
|
+
enriched.stderr = typeof stderr === "string" ? stderr : "";
|
|
105
|
+
reject(enriched);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
resolve({
|
|
109
|
+
stdout: typeof stdout === "string" ? stdout : "",
|
|
110
|
+
stderr: typeof stderr === "string" ? stderr : "",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function parseCommandError(error) {
|
|
116
|
+
const errno = error?.code;
|
|
117
|
+
if (errno === "ENOENT") {
|
|
118
|
+
return {
|
|
119
|
+
authenticated: false,
|
|
120
|
+
method: "none",
|
|
121
|
+
detail: "Codex CLI not found. Install it or set CODEX_API_KEY.",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const detail = extractErrorMessage(error) || "Not authenticated";
|
|
125
|
+
return {
|
|
126
|
+
authenticated: false,
|
|
127
|
+
method: "none",
|
|
128
|
+
detail,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function extractErrorMessage(error) {
|
|
132
|
+
if (typeof error === "object" && error !== null) {
|
|
133
|
+
const enriched = error;
|
|
134
|
+
const stderr = enriched.stderr?.trim();
|
|
135
|
+
if (stderr) {
|
|
136
|
+
return stderr;
|
|
137
|
+
}
|
|
138
|
+
const stdout = enriched.stdout?.trim();
|
|
139
|
+
if (stdout) {
|
|
140
|
+
return stdout;
|
|
141
|
+
}
|
|
142
|
+
if (enriched.signal) {
|
|
143
|
+
return `Command terminated with signal ${enriched.signal}.`;
|
|
144
|
+
}
|
|
145
|
+
if (enriched.message) {
|
|
146
|
+
return enriched.message;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return error instanceof Error ? error.message : String(error);
|
|
150
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Prefer the host Codex CLI so normal `codex` updates are picked up by the
|
|
5
|
+
* connector. Falls back to the SDK-bundled CLI when no external binary exists.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveCodexCli(env = process.env) {
|
|
8
|
+
const explicitPath = optionalString(env.CODEX_CLI_PATH);
|
|
9
|
+
if (explicitPath) {
|
|
10
|
+
return { path: explicitPath, source: "env" };
|
|
11
|
+
}
|
|
12
|
+
if (isEnabled(env.CODEX_USE_BUNDLED_CLI)) {
|
|
13
|
+
return { source: "bundled" };
|
|
14
|
+
}
|
|
15
|
+
const pathMatch = findExecutableOnPath("codex", env.PATH);
|
|
16
|
+
return pathMatch ? { path: pathMatch, source: "path" } : { source: "bundled" };
|
|
17
|
+
}
|
|
18
|
+
export function describeCodexCli(resolution) {
|
|
19
|
+
if (resolution.path) {
|
|
20
|
+
return `${resolution.source} (${resolution.path})`;
|
|
21
|
+
}
|
|
22
|
+
return "bundled @openai/codex";
|
|
23
|
+
}
|
|
24
|
+
export function findExecutableOnPath(command, pathValue) {
|
|
25
|
+
if (!pathValue) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const extensions = process.platform === "win32"
|
|
29
|
+
? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")
|
|
30
|
+
: [""];
|
|
31
|
+
for (const rawDirectory of pathValue.split(path.delimiter)) {
|
|
32
|
+
const directory = rawDirectory.trim();
|
|
33
|
+
if (!directory) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (const extension of extensions) {
|
|
37
|
+
const candidate = path.join(directory, `${command}${extension}`);
|
|
38
|
+
if (!isExecutable(candidate) || isProjectLocalBin(candidate)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function isExecutable(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
accessSync(filePath, constants.X_OK);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isProjectLocalBin(filePath) {
|
|
56
|
+
const absolute = path.resolve(filePath);
|
|
57
|
+
const resolved = resolveRealPath(filePath);
|
|
58
|
+
const localBin = path.resolve(process.cwd(), "node_modules", ".bin");
|
|
59
|
+
return (absolute === localBin ||
|
|
60
|
+
absolute.startsWith(`${localBin}${path.sep}`) ||
|
|
61
|
+
resolved === localBin ||
|
|
62
|
+
resolved.startsWith(`${localBin}${path.sep}`));
|
|
63
|
+
}
|
|
64
|
+
function resolveRealPath(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
return realpathSync(filePath);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return path.resolve(filePath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function optionalString(value) {
|
|
73
|
+
const trimmed = value?.trim();
|
|
74
|
+
return trimmed ? trimmed : undefined;
|
|
75
|
+
}
|
|
76
|
+
function isEnabled(value) {
|
|
77
|
+
const normalized = value?.trim().toLowerCase();
|
|
78
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
79
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const FAST_OPT_OUT_RE = /^(\s*fast_default_opt_out\s*=\s*)(true|false)(\s*(?:#.*)?)$/m;
|
|
4
|
+
const NOTICE_HEADER_RE = /^(\[notice\]\s*)$/m;
|
|
5
|
+
export function readCodexFastMode() {
|
|
6
|
+
const configPath = getCodexConfigPath();
|
|
7
|
+
if (!configPath || !existsSync(configPath)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const contents = readFileSync(configPath, "utf8");
|
|
12
|
+
const match = contents.match(FAST_OPT_OUT_RE);
|
|
13
|
+
if (!match) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return match[2] === "false";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function writeCodexFastMode(enabled) {
|
|
23
|
+
const configPath = getCodexConfigPath();
|
|
24
|
+
if (!configPath) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const codexDir = path.dirname(configPath);
|
|
28
|
+
mkdirSync(codexDir, { recursive: true });
|
|
29
|
+
const optOutValue = enabled ? "false" : "true";
|
|
30
|
+
const line = `fast_default_opt_out = ${optOutValue}`;
|
|
31
|
+
const currentContents = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
32
|
+
if (FAST_OPT_OUT_RE.test(currentContents)) {
|
|
33
|
+
writeFileSync(configPath, currentContents.replace(FAST_OPT_OUT_RE, `$1${optOutValue}$3`), "utf8");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (NOTICE_HEADER_RE.test(currentContents)) {
|
|
37
|
+
writeFileSync(configPath, currentContents.replace(NOTICE_HEADER_RE, `$1\n${line}`), "utf8");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const prefix = currentContents.length === 0
|
|
41
|
+
? ""
|
|
42
|
+
: currentContents.endsWith("\n")
|
|
43
|
+
? `${currentContents}\n`
|
|
44
|
+
: `${currentContents}\n\n`;
|
|
45
|
+
writeFileSync(configPath, `${prefix}[notice]\n${line}\n`, "utf8");
|
|
46
|
+
}
|
|
47
|
+
function getCodexConfigPath() {
|
|
48
|
+
const home = process.env.HOME;
|
|
49
|
+
return home ? path.join(home, ".codex", "config.toml") : null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export const DEFAULT_LAUNCH_PROFILE_ID = "default";
|
|
2
|
+
export function isCodexSandboxMode(value) {
|
|
3
|
+
return value === "read-only" || value === "workspace-write" || value === "danger-full-access";
|
|
4
|
+
}
|
|
5
|
+
export function isCodexApprovalPolicy(value) {
|
|
6
|
+
return value === "never" || value === "on-request" || value === "on-failure" || value === "untrusted";
|
|
7
|
+
}
|
|
8
|
+
export function createLaunchProfile(input) {
|
|
9
|
+
return {
|
|
10
|
+
...input,
|
|
11
|
+
unsafe: isUnsafeLaunchProfile(input.sandboxMode),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createDefaultLaunchProfile(sandboxMode, approvalPolicy) {
|
|
15
|
+
return createLaunchProfile({
|
|
16
|
+
id: DEFAULT_LAUNCH_PROFILE_ID,
|
|
17
|
+
label: "Default",
|
|
18
|
+
sandboxMode,
|
|
19
|
+
approvalPolicy,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function createBuiltinLaunchProfiles(defaultProfile, options) {
|
|
23
|
+
const profiles = [
|
|
24
|
+
defaultProfile,
|
|
25
|
+
createLaunchProfile({
|
|
26
|
+
id: "readonly",
|
|
27
|
+
label: "Read Only",
|
|
28
|
+
sandboxMode: "read-only",
|
|
29
|
+
approvalPolicy: "never",
|
|
30
|
+
}),
|
|
31
|
+
createLaunchProfile({
|
|
32
|
+
id: "review",
|
|
33
|
+
label: "Review",
|
|
34
|
+
sandboxMode: "workspace-write",
|
|
35
|
+
approvalPolicy: "on-request",
|
|
36
|
+
}),
|
|
37
|
+
];
|
|
38
|
+
if (options?.includeFullAccess) {
|
|
39
|
+
profiles.push(createLaunchProfile({
|
|
40
|
+
id: "full-access",
|
|
41
|
+
label: "Full Access",
|
|
42
|
+
sandboxMode: "danger-full-access",
|
|
43
|
+
approvalPolicy: "never",
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
return profiles;
|
|
47
|
+
}
|
|
48
|
+
export function parseLaunchProfilesJson(raw) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(raw);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new Error(`Invalid CODEX_LAUNCH_PROFILES_JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(parsed)) {
|
|
57
|
+
throw new Error("Invalid CODEX_LAUNCH_PROFILES_JSON: expected a JSON array");
|
|
58
|
+
}
|
|
59
|
+
return parsed.map((entry, index) => parseLaunchProfileEntry(entry, index));
|
|
60
|
+
}
|
|
61
|
+
export function findLaunchProfile(profiles, profileId) {
|
|
62
|
+
if (!profileId) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return profiles.find((profile) => profile.id === profileId);
|
|
66
|
+
}
|
|
67
|
+
export function formatLaunchProfileBehavior(profile) {
|
|
68
|
+
return `${profile.sandboxMode} / ${profile.approvalPolicy}`;
|
|
69
|
+
}
|
|
70
|
+
export function formatLaunchProfileLabel(profile, isCurrent = false) {
|
|
71
|
+
const prefix = profile.unsafe ? "⚠️" : "🛡️";
|
|
72
|
+
const selected = isCurrent ? " ✓" : "";
|
|
73
|
+
return `${prefix} ${profile.label} · ${formatLaunchProfileBehavior(profile)}${selected}`;
|
|
74
|
+
}
|
|
75
|
+
export function isUnsafeLaunchProfile(profileOrSandboxMode) {
|
|
76
|
+
const sandboxMode = typeof profileOrSandboxMode === "string" ? profileOrSandboxMode : profileOrSandboxMode.sandboxMode;
|
|
77
|
+
return sandboxMode === "danger-full-access";
|
|
78
|
+
}
|
|
79
|
+
function parseLaunchProfileEntry(entry, index) {
|
|
80
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
81
|
+
throw new Error(`Invalid CODEX_LAUNCH_PROFILES_JSON entry at index ${index}: expected an object`);
|
|
82
|
+
}
|
|
83
|
+
const rawId = readStringField(entry, "id", index);
|
|
84
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(rawId)) {
|
|
85
|
+
throw new Error(`Invalid CODEX_LAUNCH_PROFILES_JSON entry at index ${index}: id must match /^[a-z0-9][a-z0-9_-]*$/`);
|
|
86
|
+
}
|
|
87
|
+
const rawLabel = readStringField(entry, "label", index);
|
|
88
|
+
const rawSandboxMode = readStringField(entry, "sandboxMode", index);
|
|
89
|
+
if (!isCodexSandboxMode(rawSandboxMode)) {
|
|
90
|
+
throw new Error(`Invalid CODEX_LAUNCH_PROFILES_JSON entry at index ${index}: unsupported sandboxMode "${rawSandboxMode}"`);
|
|
91
|
+
}
|
|
92
|
+
const rawApprovalPolicy = readStringField(entry, "approvalPolicy", index);
|
|
93
|
+
if (!isCodexApprovalPolicy(rawApprovalPolicy)) {
|
|
94
|
+
throw new Error(`Invalid CODEX_LAUNCH_PROFILES_JSON entry at index ${index}: unsupported approvalPolicy "${rawApprovalPolicy}"`);
|
|
95
|
+
}
|
|
96
|
+
return createLaunchProfile({
|
|
97
|
+
id: rawId,
|
|
98
|
+
label: rawLabel,
|
|
99
|
+
sandboxMode: rawSandboxMode,
|
|
100
|
+
approvalPolicy: rawApprovalPolicy,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function readStringField(entry, field, index) {
|
|
104
|
+
const value = Reflect.get(entry, field);
|
|
105
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
106
|
+
throw new Error(`Invalid CODEX_LAUNCH_PROFILES_JSON entry at index ${index}: missing ${field}`);
|
|
107
|
+
}
|
|
108
|
+
return value.trim();
|
|
109
|
+
}
|