@nordbyte/nordrelay 0.3.1 → 0.4.0

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 (48) hide show
  1. package/.env.example +45 -2
  2. package/README.md +204 -30
  3. package/dist/agent-activity.js +300 -0
  4. package/dist/agent-adapter.js +17 -30
  5. package/dist/agent-factory.js +27 -0
  6. package/dist/agent.js +123 -9
  7. package/dist/artifacts.js +1 -1
  8. package/dist/audit-log.js +1 -1
  9. package/dist/bot-ui.js +1 -1
  10. package/dist/bot.js +328 -159
  11. package/dist/claude-code-auth.js +121 -0
  12. package/dist/claude-code-cli.js +19 -0
  13. package/dist/claude-code-launch.js +73 -0
  14. package/dist/claude-code-session.js +660 -0
  15. package/dist/claude-code-state.js +590 -0
  16. package/dist/codex-session.js +12 -1
  17. package/dist/config.js +113 -9
  18. package/dist/hermes-api.js +150 -0
  19. package/dist/hermes-auth.js +96 -0
  20. package/dist/hermes-cli.js +19 -0
  21. package/dist/hermes-launch.js +57 -0
  22. package/dist/hermes-session.js +477 -0
  23. package/dist/hermes-state.js +609 -0
  24. package/dist/index.js +51 -8
  25. package/dist/openclaw-auth.js +27 -0
  26. package/dist/openclaw-cli.js +19 -0
  27. package/dist/openclaw-gateway.js +285 -0
  28. package/dist/openclaw-launch.js +65 -0
  29. package/dist/openclaw-session.js +549 -0
  30. package/dist/openclaw-state.js +409 -0
  31. package/dist/operations.js +83 -2
  32. package/dist/pi-auth.js +59 -0
  33. package/dist/pi-launch.js +61 -0
  34. package/dist/pi-rpc.js +18 -0
  35. package/dist/pi-session.js +103 -15
  36. package/dist/pi-state.js +253 -0
  37. package/dist/relay-runtime.js +673 -51
  38. package/dist/session-format.js +28 -18
  39. package/dist/session-registry.js +40 -15
  40. package/dist/settings-service.js +35 -4
  41. package/dist/web-dashboard-ui.js +18 -0
  42. package/dist/web-dashboard.js +329 -47
  43. package/package.json +8 -3
  44. package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
  45. package/plugins/nordrelay/commands/remote.md +2 -2
  46. package/plugins/nordrelay/scripts/nordrelay.mjs +131 -3
  47. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
  48. package/CHANGELOG.md +0 -26
@@ -0,0 +1,409 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export function getDefaultOpenClawHome() {
5
+ return path.join(os.homedir(), ".openclaw");
6
+ }
7
+ export function resolveOpenClawStateDir(options = {}) {
8
+ return options.stateDir
9
+ ?? process.env.OPENCLAW_STATE_DIR
10
+ ?? options.openClawHome
11
+ ?? process.env.OPENCLAW_HOME
12
+ ?? getDefaultOpenClawHome();
13
+ }
14
+ export function listOpenClawSessions(limit = 20, options = {}) {
15
+ const payload = options.sessionsJson ?? readOpenClawSessionsJson(limit, options);
16
+ return parseOpenClawSessionsPayload(payload, options)
17
+ .sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())
18
+ .slice(0, Math.max(1, limit));
19
+ }
20
+ export function getOpenClawSession(id, options = {}) {
21
+ const normalized = id.trim();
22
+ if (!normalized) {
23
+ return null;
24
+ }
25
+ const sessions = listOpenClawSessions(500, options);
26
+ return sessions.find((record) => record.id === normalized || record.sessionKey === normalized)
27
+ ?? sessions.find((record) => record.id.startsWith(normalized) || record.sessionKey.startsWith(normalized))
28
+ ?? null;
29
+ }
30
+ export function listOpenClawWorkspaces(options = {}) {
31
+ const workspaces = new Set();
32
+ for (const record of listOpenClawSessions(500, options)) {
33
+ if (record.cwd) {
34
+ workspaces.add(record.cwd);
35
+ }
36
+ }
37
+ if (options.workspace) {
38
+ workspaces.add(options.workspace);
39
+ }
40
+ return [...workspaces].sort((left, right) => left.localeCompare(right));
41
+ }
42
+ export function getOpenClawSessionActivity(id, options = {}) {
43
+ return getOpenClawSessionSnapshot(id, { ...options, maxEvents: 0 })?.activity ?? null;
44
+ }
45
+ export function getOpenClawSessionActivityLog(id, limit = 50, options = {}) {
46
+ return getOpenClawSessionSnapshot(id, { ...options, maxEvents: Math.max(1, limit) })?.events ?? [];
47
+ }
48
+ export function getOpenClawSessionSnapshot(id, options = {}) {
49
+ const record = getOpenClawSession(id, options);
50
+ if (!record) {
51
+ return null;
52
+ }
53
+ const events = parseOpenClawActivityEvents(record, options.afterLine ?? 0);
54
+ const latestUser = [...events].reverse().find((event) => event.kind === "user");
55
+ const latestAgent = [...events].reverse().find((event) => event.kind === "agent");
56
+ const latestTerminal = [...events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
57
+ const latestTool = [...events].reverse().find((event) => event.kind === "tool" && event.toolName);
58
+ const latestTimestamp = events.at(-1)?.timestamp ?? record.updatedAt;
59
+ const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
60
+ const nowMs = options.nowMs ?? Date.now();
61
+ const stale = Boolean(record.active && latestTimestamp && nowMs - latestTimestamp.getTime() > staleAfterMs);
62
+ const maxEvents = options.maxEvents ?? 50;
63
+ const lineCount = Math.max(events.length, record.active ? 1 : 0);
64
+ const returnedEvents = maxEvents <= 0 ? [] : events.slice(-maxEvents);
65
+ return {
66
+ agentId: "openclaw",
67
+ agentLabel: "OpenClaw",
68
+ threadId: record.id,
69
+ sourcePath: record.sessionPath ?? sourcePath(options),
70
+ sourceLabel: "OpenClaw sessions",
71
+ lineCount,
72
+ activity: {
73
+ agentId: "openclaw",
74
+ agentLabel: "OpenClaw",
75
+ threadId: record.id,
76
+ sourcePath: record.sessionPath ?? sourcePath(options),
77
+ sourceLabel: "OpenClaw sessions",
78
+ active: record.active && !stale,
79
+ stale,
80
+ turnId: latestUser?.turnId ?? latestTerminal?.turnId ?? record.id,
81
+ startedAt: latestUser?.timestamp ?? (record.active ? record.updatedAt : null),
82
+ updatedAt: latestTimestamp,
83
+ },
84
+ events: returnedEvents,
85
+ latestAgentMessage: latestAgent?.text ?? null,
86
+ latestUserMessage: latestUser?.text ?? record.firstUserMessage,
87
+ latestToolName: latestTool?.toolName ?? null,
88
+ };
89
+ }
90
+ export function getOpenClawSessionDiagnostics(id, options = {}) {
91
+ const source = sourcePath(options);
92
+ if (!id) {
93
+ return {
94
+ sourcePath: source,
95
+ status: "unavailable",
96
+ reason: "no active OpenClaw session",
97
+ lineCount: 0,
98
+ updatedAt: null,
99
+ };
100
+ }
101
+ const snapshot = getOpenClawSessionSnapshot(id, { ...options, maxEvents: 0 });
102
+ if (!snapshot) {
103
+ return {
104
+ sourcePath: source,
105
+ status: "unavailable",
106
+ reason: "OpenClaw session not found",
107
+ lineCount: 0,
108
+ updatedAt: null,
109
+ };
110
+ }
111
+ const status = snapshot.activity.active ? "active" : snapshot.activity.stale ? "stale" : "idle";
112
+ const reason = snapshot.activity.active
113
+ ? "OpenClaw session reports an active run"
114
+ : snapshot.activity.stale
115
+ ? "OpenClaw active run exceeded stale timeout"
116
+ : "OpenClaw session is idle";
117
+ return {
118
+ sourcePath: snapshot.sourcePath,
119
+ status,
120
+ reason,
121
+ lineCount: snapshot.lineCount,
122
+ updatedAt: snapshot.activity.updatedAt,
123
+ };
124
+ }
125
+ export function parseOpenClawSessionsPayload(payload, options = {}) {
126
+ const root = objectValue(payload);
127
+ const storePaths = new Map();
128
+ for (const store of arrayValue(root?.stores)) {
129
+ const storeObject = objectValue(store);
130
+ const agentId = stringValue(storeObject?.agentId) ?? stringValue(storeObject?.agent_id);
131
+ const storePath = stringValue(storeObject?.path) ?? stringValue(storeObject?.storePath);
132
+ if (agentId && storePath) {
133
+ storePaths.set(agentId, storePath);
134
+ }
135
+ }
136
+ const sessions = arrayValue(root?.sessions ?? payload);
137
+ return sessions
138
+ .map((entry) => mapOpenClawSession(entry, options, storePaths))
139
+ .filter((record) => Boolean(record));
140
+ }
141
+ function readOpenClawSessionsJson(limit, options) {
142
+ const cliPath = options.cliPath ?? process.env.OPENCLAW_CLI_PATH ?? "openclaw";
143
+ const args = ["sessions", "--all-agents", "--limit", String(Math.max(1, limit)), "--json"];
144
+ const result = spawnSync(cliPath, args, {
145
+ encoding: "utf8",
146
+ timeout: 5000,
147
+ windowsHide: true,
148
+ env: process.env,
149
+ });
150
+ if (result.error || result.status !== 0) {
151
+ return { sessions: [] };
152
+ }
153
+ try {
154
+ return JSON.parse(result.stdout.trim() || "{}");
155
+ }
156
+ catch {
157
+ return { sessions: [] };
158
+ }
159
+ }
160
+ function mapOpenClawSession(raw, options, storePaths) {
161
+ const object = objectValue(raw);
162
+ if (!object) {
163
+ return null;
164
+ }
165
+ const key = stringValue(object.key)
166
+ ?? stringValue(object.sessionKey)
167
+ ?? stringValue(object.session_key)
168
+ ?? stringValue(object.id)
169
+ ?? stringValue(object.sessionId)
170
+ ?? stringValue(object.session_id);
171
+ if (!key) {
172
+ return null;
173
+ }
174
+ const id = stringValue(object.id)
175
+ ?? stringValue(object.sessionId)
176
+ ?? stringValue(object.session_id)
177
+ ?? key;
178
+ const openClawAgentId = stringValue(object.agentId)
179
+ ?? stringValue(object.agent_id)
180
+ ?? parseAgentFromSessionKey(key)
181
+ ?? options.openClawAgentId
182
+ ?? null;
183
+ const sessionPath = stringValue(object.path)
184
+ ?? stringValue(object.storePath)
185
+ ?? stringValue(object.store_path)
186
+ ?? (openClawAgentId ? storePaths.get(openClawAgentId) : undefined);
187
+ const workspace = stringValue(object.workspace)
188
+ ?? stringValue(object.cwd)
189
+ ?? workspaceFromSource(stringValue(object.source))
190
+ ?? options.workspace
191
+ ?? process.cwd();
192
+ const createdAt = dateValue(object.createdAt)
193
+ ?? dateValue(object.created_at)
194
+ ?? dateValue(object.startedAt)
195
+ ?? dateValue(object.started_at)
196
+ ?? new Date();
197
+ const updatedAt = dateValue(object.updatedAt)
198
+ ?? dateValue(object.updated_at)
199
+ ?? dateValue(object.lastActive)
200
+ ?? dateValue(object.last_active)
201
+ ?? dateValue(object.endedAt)
202
+ ?? dateValue(object.ended_at)
203
+ ?? createdAt;
204
+ const firstUserMessage = stringValue(object.firstUserMessage)
205
+ ?? stringValue(object.first_user_message)
206
+ ?? firstMessageText(object);
207
+ const status = stringValue(object.status);
208
+ const active = booleanValue(object.active)
209
+ ?? booleanValue(object.running)
210
+ ?? booleanValue(object.inProgress)
211
+ ?? booleanValue(object.in_progress)
212
+ ?? booleanValue(object.isRunning)
213
+ ?? statusIsActive(status);
214
+ return {
215
+ id,
216
+ sessionKey: key,
217
+ title: stringValue(object.title),
218
+ cwd: workspace,
219
+ model: stringValue(object.model),
220
+ reasoningEffort: stringValue(object.thinking)
221
+ ?? stringValue(object.thinkingLevel)
222
+ ?? stringValue(object.thinking_level)
223
+ ?? stringValue(object.reasoningEffort)
224
+ ?? stringValue(object.reasoning_effort),
225
+ createdAt,
226
+ updatedAt,
227
+ firstUserMessage,
228
+ agentId: "openclaw",
229
+ sessionPath,
230
+ openClawAgentId,
231
+ status,
232
+ active,
233
+ usage: usageFromSession(object),
234
+ raw,
235
+ };
236
+ }
237
+ function parseOpenClawActivityEvents(record, afterLine) {
238
+ const raw = objectValue(record.raw);
239
+ const sourceEvents = arrayValue(raw?.events ?? raw?.messages ?? raw?.turns ?? raw?.transcript);
240
+ const events = [];
241
+ let lineNumber = 0;
242
+ if (record.active) {
243
+ lineNumber += 1;
244
+ events.push({
245
+ lineNumber,
246
+ kind: "task",
247
+ timestamp: record.updatedAt,
248
+ type: "run",
249
+ turnId: record.id,
250
+ status: "started",
251
+ text: record.status,
252
+ toolName: null,
253
+ phase: null,
254
+ });
255
+ }
256
+ for (const event of sourceEvents) {
257
+ const object = objectValue(event);
258
+ if (!object)
259
+ continue;
260
+ lineNumber += 1;
261
+ const parsed = mapActivityObject(object, lineNumber, record.id);
262
+ if (parsed)
263
+ events.push(parsed);
264
+ }
265
+ if (!record.active && events.some((event) => event.kind === "user") && !events.some((event) => event.kind === "task" && event.status !== "started")) {
266
+ lineNumber += 1;
267
+ events.push({
268
+ lineNumber,
269
+ kind: "task",
270
+ timestamp: record.updatedAt,
271
+ type: "run",
272
+ turnId: record.id,
273
+ status: statusIsFailure(record.status) ? "failed" : "completed",
274
+ text: record.status,
275
+ toolName: null,
276
+ phase: null,
277
+ });
278
+ }
279
+ if (events.length === 0 && record.firstUserMessage) {
280
+ events.push({
281
+ lineNumber: 1,
282
+ kind: "user",
283
+ timestamp: record.createdAt,
284
+ type: "message",
285
+ turnId: record.id,
286
+ status: null,
287
+ text: record.firstUserMessage,
288
+ toolName: null,
289
+ phase: null,
290
+ });
291
+ }
292
+ return events.filter((event) => event.lineNumber > afterLine);
293
+ }
294
+ function mapActivityObject(object, lineNumber, fallbackTurnId) {
295
+ const role = stringValue(object.role)?.toLowerCase();
296
+ const type = stringValue(object.type) ?? stringValue(object.event) ?? role ?? "message";
297
+ const status = stringValue(object.status);
298
+ const timestamp = dateValue(object.timestamp) ?? dateValue(object.createdAt) ?? dateValue(object.created_at);
299
+ const text = stringValue(object.text)
300
+ ?? stringValue(object.content)
301
+ ?? stringValue(object.message)
302
+ ?? stringValue(object.summary);
303
+ const toolName = stringValue(object.toolName)
304
+ ?? stringValue(object.tool_name)
305
+ ?? stringValue(object.name)
306
+ ?? stringValue(object.tool);
307
+ const turnId = stringValue(object.turnId) ?? stringValue(object.turn_id) ?? stringValue(object.runId) ?? fallbackTurnId;
308
+ if (role === "user") {
309
+ return { lineNumber, kind: "user", timestamp, type, turnId, status, text, toolName: null, phase: null };
310
+ }
311
+ if (role === "assistant" || role === "agent") {
312
+ return { lineNumber, kind: "agent", timestamp, type, turnId, status, text, toolName: null, phase: stringValue(object.phase) };
313
+ }
314
+ if (role === "tool" || toolName || type.includes("tool")) {
315
+ return {
316
+ lineNumber,
317
+ kind: "tool",
318
+ timestamp,
319
+ type,
320
+ turnId,
321
+ status: status === "completed" ? "finished" : status ?? (type.includes("start") ? "started" : "finished"),
322
+ text,
323
+ toolName: toolName ?? "tool",
324
+ phase: null,
325
+ };
326
+ }
327
+ if (type.includes("run") || type.includes("task")) {
328
+ return { lineNumber, kind: "task", timestamp, type, turnId, status, text, toolName: null, phase: null };
329
+ }
330
+ return text ? { lineNumber, kind: "agent", timestamp, type, turnId, status, text, toolName: null, phase: null } : null;
331
+ }
332
+ function sourcePath(options) {
333
+ return path.join(resolveOpenClawStateDir(options), "sessions");
334
+ }
335
+ function parseAgentFromSessionKey(key) {
336
+ const match = key.match(/^agent:([^:]+):/);
337
+ return match?.[1] ?? null;
338
+ }
339
+ function workspaceFromSource(source) {
340
+ if (!source)
341
+ return null;
342
+ if (path.isAbsolute(source))
343
+ return source;
344
+ const match = source.match(/(?:cwd|workspace)=([^,;]+)/i);
345
+ return match && path.isAbsolute(match[1]) ? match[1] : null;
346
+ }
347
+ function firstMessageText(object) {
348
+ for (const entry of arrayValue(object.messages ?? object.transcript ?? object.events)) {
349
+ const row = objectValue(entry);
350
+ if (stringValue(row?.role)?.toLowerCase() === "user") {
351
+ return stringValue(row?.text) ?? stringValue(row?.content) ?? stringValue(row?.message);
352
+ }
353
+ }
354
+ return null;
355
+ }
356
+ function usageFromSession(object) {
357
+ const usage = objectValue(object.usage) ?? object;
358
+ const input = numberValue(usage.input) ?? numberValue(usage.inputTokens) ?? numberValue(usage.input_tokens) ?? 0;
359
+ const output = numberValue(usage.output) ?? numberValue(usage.outputTokens) ?? numberValue(usage.output_tokens) ?? 0;
360
+ const cacheRead = numberValue(usage.cacheRead) ?? numberValue(usage.cache_read_tokens) ?? 0;
361
+ const cacheWrite = numberValue(usage.cacheWrite) ?? numberValue(usage.cache_write_tokens) ?? 0;
362
+ const total = input + output + cacheRead + cacheWrite;
363
+ if (total <= 0) {
364
+ return undefined;
365
+ }
366
+ return {
367
+ input,
368
+ output,
369
+ cacheRead,
370
+ cacheWrite,
371
+ total,
372
+ cost: numberValue(usage.cost) ?? numberValue(usage.estimated_cost_usd) ?? undefined,
373
+ };
374
+ }
375
+ function statusIsActive(status) {
376
+ return Boolean(status && /^(active|running|processing|queued|pending|accepted)$/i.test(status));
377
+ }
378
+ function statusIsFailure(status) {
379
+ return Boolean(status && /^(failed|error)$/i.test(status));
380
+ }
381
+ function arrayValue(value) {
382
+ return Array.isArray(value) ? value : [];
383
+ }
384
+ function objectValue(value) {
385
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
386
+ }
387
+ function stringValue(value) {
388
+ if (typeof value === "string" && value.trim())
389
+ return value;
390
+ if (typeof value === "number" && Number.isFinite(value))
391
+ return String(value);
392
+ return null;
393
+ }
394
+ function numberValue(value) {
395
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
396
+ }
397
+ function booleanValue(value) {
398
+ return typeof value === "boolean" ? value : null;
399
+ }
400
+ function dateValue(value) {
401
+ if (typeof value === "number" && Number.isFinite(value)) {
402
+ return new Date(value > 10_000_000_000 ? value : value * 1000);
403
+ }
404
+ if (typeof value === "string" && value.trim()) {
405
+ const parsed = new Date(value);
406
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
407
+ }
408
+ return null;
409
+ }
@@ -5,11 +5,18 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { describeCodexCli, resolveCodexCli } from "./codex-cli.js";
7
7
  import { findLatestDatabase } from "./codex-state.js";
8
+ import { describeClaudeCodeCli, resolveClaudeCodeCli } from "./claude-code-cli.js";
9
+ import { describeHermesCli, resolveHermesCli } from "./hermes-cli.js";
10
+ import { describeOpenClawCli, resolveOpenClawCli } from "./openclaw-cli.js";
8
11
  import { describePiCli, resolvePiCli } from "./pi-cli.js";
9
12
  const APP_NAME = "nordrelay";
10
13
  const PACKAGE_NAME = "@nordbyte/nordrelay";
11
14
  const CODEX_PACKAGE_NAME = "@openai/codex";
12
15
  const PI_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
16
+ const HERMES_PACKAGE_NAME = "hermes-agent";
17
+ const OPENCLAW_PACKAGE_NAME = "openclaw";
18
+ const CLAUDE_CODE_PACKAGE_NAME = "@anthropic-ai/claude-code";
19
+ const CLAUDE_CODE_SDK_PACKAGE_NAME = "@anthropic-ai/claude-agent-sdk";
13
20
  const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
14
21
  const SECRET_RE = /(bot|token|api[_-]?key|authorization|bearer|password|secret)(["'=: ]+)([^\s"',]+)/gi;
15
22
  const DEFAULT_VERSION_CACHE_TTL_MS = 60 * 60 * 1000;
@@ -80,10 +87,19 @@ export async function getVersionChecks(options = {}) {
80
87
  const nordrelayVersion = await getPackageVersion();
81
88
  const codexCli = resolveCodexCli();
82
89
  const piCli = resolvePiCli(process.env, options.piCliPath);
90
+ const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
91
+ const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
92
+ const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
83
93
  const codexVersionLabel = codexCli.path
84
94
  ? detectCliVersion(codexCli.path)
85
95
  : readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed";
86
96
  const piVersionLabel = piCli.path ? detectCliVersion(piCli.path) : "not installed";
97
+ const hermesVersionLabel = hermesCli.path ? detectCliVersion(hermesCli.path) : "not installed";
98
+ const openClawVersionLabel = openClawCli.path ? detectCliVersion(openClawCli.path) : "not installed";
99
+ const claudeCodeVersionLabel = claudeCodeCli.path
100
+ ? detectCliVersion(claudeCodeCli.path)
101
+ : readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled";
102
+ const claudeCodePackageName = claudeCodeCli.path ? CLAUDE_CODE_PACKAGE_NAME : CLAUDE_CODE_SDK_PACKAGE_NAME;
87
103
  return {
88
104
  nordrelay: buildVersionCheck({
89
105
  label: "NordRelay",
@@ -105,15 +121,33 @@ export async function getVersionChecks(options = {}) {
105
121
  installedVersion: extractVersion(piVersionLabel),
106
122
  notInstalled: piVersionLabel === "not installed",
107
123
  }),
124
+ hermes: buildHermesVersionCheck(hermesVersionLabel),
125
+ openclaw: buildVersionCheck({
126
+ label: "OpenClaw",
127
+ packageName: OPENCLAW_PACKAGE_NAME,
128
+ installedLabel: openClawVersionLabel,
129
+ installedVersion: extractVersion(openClawVersionLabel),
130
+ notInstalled: openClawVersionLabel === "not installed",
131
+ }),
132
+ claudeCode: buildVersionCheck({
133
+ label: "Claude Code",
134
+ packageName: claudeCodePackageName,
135
+ installedLabel: claudeCodeVersionLabel,
136
+ installedVersion: extractVersion(claudeCodeVersionLabel),
137
+ notInstalled: claudeCodeVersionLabel === "not installed",
138
+ }),
108
139
  };
109
140
  }
110
- export async function getConnectorHealth() {
141
+ export async function getConnectorHealth(options = {}) {
111
142
  const state = await readConnectorState();
112
143
  const version = await getPackageVersion();
113
144
  const pidRunning = isProcessRunning(state.pid);
114
145
  const appPidRunning = isProcessRunning(state.appPid);
115
146
  const codexCli = resolveCodexCli();
116
- const piCli = resolvePiCli();
147
+ const piCli = resolvePiCli(process.env, options.piCliPath);
148
+ const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
149
+ const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
150
+ const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
117
151
  return {
118
152
  version,
119
153
  state,
@@ -125,6 +159,17 @@ export async function getConnectorHealth() {
125
159
  piCli: describePiCli(piCli),
126
160
  piCliPath: piCli.path ?? null,
127
161
  piCliVersion: detectCliVersion(piCli.path),
162
+ hermesCli: describeHermesCli(hermesCli),
163
+ hermesCliPath: hermesCli.path ?? null,
164
+ hermesCliVersion: detectCliVersion(hermesCli.path),
165
+ openClawCli: describeOpenClawCli(openClawCli),
166
+ openClawCliPath: openClawCli.path ?? null,
167
+ openClawCliVersion: detectCliVersion(openClawCli.path),
168
+ claudeCodeCli: describeClaudeCodeCli(claudeCodeCli),
169
+ claudeCodeCliPath: claudeCodeCli.path ?? null,
170
+ claudeCodeCliVersion: claudeCodeCli.path
171
+ ? detectCliVersion(claudeCodeCli.path)
172
+ : readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled",
128
173
  stateFile: getConnectorStatePath(),
129
174
  logFile: getConnectorLogPath(),
130
175
  databasePath: findLatestDatabase(),
@@ -249,6 +294,31 @@ function detectCliVersion(commandPath) {
249
294
  }
250
295
  return output || "unknown";
251
296
  }
297
+ function buildHermesVersionCheck(installedLabel) {
298
+ if (installedLabel === "not installed") {
299
+ return {
300
+ label: "Hermes",
301
+ packageName: HERMES_PACKAGE_NAME,
302
+ installedLabel: "not installed",
303
+ installedVersion: null,
304
+ latestVersion: null,
305
+ status: "not-installed",
306
+ };
307
+ }
308
+ const lines = installedLabel.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
309
+ const versionLine = lines[0] ?? installedLabel;
310
+ const updateLine = lines.find((line) => /^Update available:/i.test(line));
311
+ const installedVersion = extractVersion(versionLine);
312
+ return {
313
+ label: "Hermes",
314
+ packageName: HERMES_PACKAGE_NAME,
315
+ installedLabel: versionLine,
316
+ installedVersion,
317
+ latestVersion: updateLine?.replace(/^Update available:\s*/i, "") ?? null,
318
+ status: updateLine ? "outdated" : installedVersion ? "current" : "unknown",
319
+ detail: updateLine ?? (installedVersion ? undefined : "Could not parse Hermes version or update status"),
320
+ };
321
+ }
252
322
  function buildVersionCheck(options) {
253
323
  if (options.notInstalled) {
254
324
  return {
@@ -260,6 +330,17 @@ function buildVersionCheck(options) {
260
330
  status: "not-installed",
261
331
  };
262
332
  }
333
+ if (options.skipLatest) {
334
+ return {
335
+ label: options.label,
336
+ packageName: options.packageName,
337
+ installedLabel: options.installedLabel,
338
+ installedVersion: options.installedVersion,
339
+ latestVersion: null,
340
+ status: options.installedVersion ? "unknown" : "unknown",
341
+ detail: "Latest-version lookup is not available for this package source",
342
+ };
343
+ }
263
344
  const latest = detectLatestNpmVersion(options.packageName);
264
345
  if (!options.installedVersion || !latest.version) {
265
346
  return {
@@ -0,0 +1,59 @@
1
+ const PROVIDER_ENV_KEYS = {
2
+ anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
3
+ "aws-bedrock": ["AWS_BEARER_TOKEN_BEDROCK", "AWS_PROFILE", "AWS_ACCESS_KEY_ID"],
4
+ azure: ["AZURE_OPENAI_API_KEY"],
5
+ "azure-openai": ["AZURE_OPENAI_API_KEY"],
6
+ cerebras: ["CEREBRAS_API_KEY"],
7
+ cloudflare: ["CLOUDFLARE_API_KEY"],
8
+ deepseek: ["DEEPSEEK_API_KEY"],
9
+ fireworks: ["FIREWORKS_API_KEY"],
10
+ gemini: ["GEMINI_API_KEY"],
11
+ google: ["GEMINI_API_KEY"],
12
+ groq: ["GROQ_API_KEY"],
13
+ kimi: ["KIMI_API_KEY"],
14
+ minimax: ["MINIMAX_API_KEY"],
15
+ mistral: ["MISTRAL_API_KEY"],
16
+ moonshot: ["MOONSHOT_API_KEY"],
17
+ opencode: ["OPENCODE_API_KEY"],
18
+ openrouter: ["OPENROUTER_API_KEY"],
19
+ openai: ["OPENAI_API_KEY"],
20
+ "openai-codex": ["OPENAI_API_KEY"],
21
+ xai: ["XAI_API_KEY"],
22
+ xiaomi: ["XIAOMI_API_KEY", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "XIAOMI_TOKEN_PLAN_AMS_API_KEY", "XIAOMI_TOKEN_PLAN_SGP_API_KEY"],
23
+ zai: ["ZAI_API_KEY"],
24
+ };
25
+ export function checkPiAuthStatus(model, env = process.env) {
26
+ const provider = providerFromModel(model);
27
+ const keys = PROVIDER_ENV_KEYS[provider];
28
+ if (!keys) {
29
+ return {
30
+ authenticated: true,
31
+ method: "cli",
32
+ detail: `Pi provider "${provider}" is not verifiable by NordRelay. Run "pi" on the host if auth fails.`,
33
+ };
34
+ }
35
+ const configured = keys.filter((key) => Boolean(env[key]?.trim()));
36
+ if (configured.length > 0) {
37
+ return {
38
+ authenticated: true,
39
+ method: "api-key",
40
+ detail: `Pi provider "${provider}" has ${configured.join(" or ")} configured.`,
41
+ };
42
+ }
43
+ return {
44
+ authenticated: false,
45
+ method: "none",
46
+ detail: `Pi provider "${provider}" needs one of: ${keys.join(", ")}.`,
47
+ };
48
+ }
49
+ function providerFromModel(model) {
50
+ const trimmed = model?.trim();
51
+ if (!trimmed) {
52
+ return "google";
53
+ }
54
+ const separator = trimmed.indexOf("/");
55
+ if (separator === -1) {
56
+ return "google";
57
+ }
58
+ return trimmed.slice(0, separator).toLowerCase();
59
+ }
@@ -0,0 +1,61 @@
1
+ import { createLaunchProfile } from "./codex-launch.js";
2
+ export const PI_LAUNCH_PROFILES = [
3
+ {
4
+ id: "default",
5
+ label: "Default",
6
+ behavior: "all tools / online / extensions",
7
+ unsafe: false,
8
+ cli: {},
9
+ },
10
+ {
11
+ id: "readonly",
12
+ label: "Read-only",
13
+ behavior: "read, grep, find, ls / online",
14
+ unsafe: false,
15
+ cli: { tools: "read,grep,find,ls" },
16
+ },
17
+ {
18
+ id: "no-tools",
19
+ label: "No tools",
20
+ behavior: "no tools / online",
21
+ unsafe: false,
22
+ cli: { noTools: true },
23
+ },
24
+ {
25
+ id: "offline",
26
+ label: "Offline",
27
+ behavior: "all tools / offline / extensions",
28
+ unsafe: false,
29
+ cli: { offline: true },
30
+ },
31
+ {
32
+ id: "safe-offline",
33
+ label: "Safe Offline",
34
+ behavior: "read-only tools / offline / no extensions",
35
+ unsafe: false,
36
+ cli: { tools: "read,grep,find,ls", offline: true, noExtensions: true },
37
+ },
38
+ ];
39
+ export function listPiLaunchProfiles() {
40
+ return PI_LAUNCH_PROFILES.map((profile) => ({
41
+ id: profile.id,
42
+ label: profile.label,
43
+ behavior: profile.behavior,
44
+ unsafe: profile.unsafe,
45
+ }));
46
+ }
47
+ export function findPiLaunchProfile(profileId) {
48
+ const profile = PI_LAUNCH_PROFILES.find((candidate) => candidate.id === profileId);
49
+ if (profile) {
50
+ return profile;
51
+ }
52
+ return PI_LAUNCH_PROFILES[0];
53
+ }
54
+ export function piProfileAsLaunchProfile(profile) {
55
+ return createLaunchProfile({
56
+ id: profile.id,
57
+ label: profile.label,
58
+ sandboxMode: "workspace-write",
59
+ approvalPolicy: "never",
60
+ });
61
+ }
package/dist/pi-rpc.js CHANGED
@@ -68,6 +68,24 @@ export class PiRpcClient {
68
68
  if (this.options.thinking) {
69
69
  args.push("--thinking", this.options.thinking);
70
70
  }
71
+ if (this.options.tools) {
72
+ args.push("--tools", this.options.tools);
73
+ }
74
+ if (this.options.noTools) {
75
+ args.push("--no-tools");
76
+ }
77
+ if (this.options.noBuiltinTools) {
78
+ args.push("--no-builtin-tools");
79
+ }
80
+ if (this.options.offline) {
81
+ args.push("--offline");
82
+ }
83
+ if (this.options.noExtensions) {
84
+ args.push("--no-extensions");
85
+ }
86
+ if (this.options.noSkills) {
87
+ args.push("--no-skills");
88
+ }
71
89
  const child = spawn(this.options.commandPath, args, {
72
90
  cwd: this.options.cwd,
73
91
  env: { ...process.env, ...this.options.env },