From 1721d044053d7a966fb1e0954385bb764d5ee26d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 15:59:39 +0000 Subject: [PATCH] feat: add node core/ui versions in bridge --- src/agents/tools/nodes-utils.ts | 7 ++++ src/cli/nodes-cli/register.status.ts | 48 ++++++++++++++++++++++++++- src/cli/nodes-cli/types.ts | 6 ++++ src/gateway/protocol/schema/nodes.ts | 2 ++ src/gateway/server-methods/nodes.ts | 8 +++++ src/gateway/server-node-bridge.ts | 23 ++++++++++++- src/infra/bridge/server/connection.ts | 8 +++++ src/infra/bridge/server/types.ts | 6 ++++ src/infra/node-pairing.ts | 10 ++++++ src/node-host/bridge-client.ts | 6 ++++ src/node-host/runner.ts | 1 + 11 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/agents/tools/nodes-utils.ts b/src/agents/tools/nodes-utils.ts index 4e9ffe014..04cda45c4 100644 --- a/src/agents/tools/nodes-utils.ts +++ b/src/agents/tools/nodes-utils.ts @@ -4,6 +4,9 @@ export type NodeListNode = { nodeId: string; displayName?: string; platform?: string; + version?: string; + coreVersion?: string; + uiVersion?: string; remoteIp?: string; deviceFamily?: string; modelIdentifier?: string; @@ -20,6 +23,8 @@ type PendingRequest = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; remoteIp?: string; isRepair?: boolean; ts: number; @@ -31,6 +36,8 @@ type PairedNode = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; remoteIp?: string; permissions?: Record; createdAtMs?: number; diff --git a/src/cli/nodes-cli/register.status.ts b/src/cli/nodes-cli/register.status.ts index 5b671aa65..25dd543d5 100644 --- a/src/cli/nodes-cli/register.status.ts +++ b/src/cli/nodes-cli/register.status.ts @@ -4,6 +4,43 @@ import { formatAge, formatPermissions, parseNodeList, parsePairingList } from ". import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; +function formatVersionLabel(raw: string) { + const trimmed = raw.trim(); + if (!trimmed) return raw; + if (trimmed.toLowerCase().startsWith("v")) return trimmed; + return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed; +} + +function resolveNodeVersions(node: { + platform?: string; + version?: string; + coreVersion?: string; + uiVersion?: string; +}) { + const core = node.coreVersion?.trim() || undefined; + const ui = node.uiVersion?.trim() || undefined; + if (core || ui) return { core, ui }; + const legacy = node.version?.trim(); + if (!legacy) return { core: undefined, ui: undefined }; + const platform = node.platform?.trim().toLowerCase() ?? ""; + const headless = + platform === "darwin" || platform === "linux" || platform === "win32" || platform === "windows"; + return headless ? { core: legacy, ui: undefined } : { core: undefined, ui: legacy }; +} + +function formatNodeVersions(node: { + platform?: string; + version?: string; + coreVersion?: string; + uiVersion?: string; +}) { + const { core, ui } = resolveNodeVersions(node); + const parts: string[] = []; + if (core) parts.push(`core ${formatVersionLabel(core)}`); + if (ui) parts.push(`ui ${formatVersionLabel(ui)}`); + return parts.length > 0 ? parts.join(" · ") : null; +} + export function registerNodesStatusCommands(nodes: Command) { nodesCallOpts( nodes @@ -29,6 +66,8 @@ export function registerNodesStatusCommands(nodes: Command) { const hw = n.modelIdentifier ? ` · hw: ${n.modelIdentifier}` : ""; const perms = formatPermissions(n.permissions); const permsText = perms ? ` · perms: ${perms}` : ""; + const versions = formatNodeVersions(n); + const versionText = versions ? ` · ${versions}` : ""; const caps = Array.isArray(n.caps) && n.caps.length > 0 ? `[${n.caps.map(String).filter(Boolean).sort().join(",")}]` @@ -37,7 +76,7 @@ export function registerNodesStatusCommands(nodes: Command) { : "?"; const pairing = n.paired ? "paired" : "unpaired"; defaultRuntime.log( - `- ${name} · ${n.nodeId}${ip}${device}${hw}${permsText} · ${pairing} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`, + `- ${name} · ${n.nodeId}${ip}${device}${hw}${permsText}${versionText} · ${pairing} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`, ); } } catch (err) { @@ -77,12 +116,19 @@ export function registerNodesStatusCommands(nodes: Command) { const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null; const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null; const ip = typeof obj.remoteIp === "string" ? obj.remoteIp : null; + const versions = formatNodeVersions(obj as { + platform?: string; + version?: string; + coreVersion?: string; + uiVersion?: string; + }); const parts: string[] = ["Node:", displayName, nodeId]; if (ip) parts.push(ip); if (family) parts.push(`device: ${family}`); if (model) parts.push(`hw: ${model}`); if (perms) parts.push(`perms: ${perms}`); + if (versions) parts.push(versions); parts.push(connected ? "connected" : "disconnected"); parts.push(`caps: ${caps ? `[${caps.join(",")}]` : "?"}`); defaultRuntime.log(parts.join(" · ")); diff --git a/src/cli/nodes-cli/types.ts b/src/cli/nodes-cli/types.ts index 6ec97d885..9c0cf67b4 100644 --- a/src/cli/nodes-cli/types.ts +++ b/src/cli/nodes-cli/types.ts @@ -46,6 +46,8 @@ export type NodeListNode = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; remoteIp?: string; deviceFamily?: string; modelIdentifier?: string; @@ -62,6 +64,8 @@ export type PendingRequest = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; remoteIp?: string; isRepair?: boolean; ts: number; @@ -73,6 +77,8 @@ export type PairedNode = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; remoteIp?: string; permissions?: Record; createdAtMs?: number; diff --git a/src/gateway/protocol/schema/nodes.ts b/src/gateway/protocol/schema/nodes.ts index ed1b67eee..fdfb47ed5 100644 --- a/src/gateway/protocol/schema/nodes.ts +++ b/src/gateway/protocol/schema/nodes.ts @@ -8,6 +8,8 @@ export const NodePairRequestParamsSchema = Type.Object( displayName: Type.Optional(NonEmptyString), platform: Type.Optional(NonEmptyString), version: Type.Optional(NonEmptyString), + coreVersion: Type.Optional(NonEmptyString), + uiVersion: Type.Optional(NonEmptyString), deviceFamily: Type.Optional(NonEmptyString), modelIdentifier: Type.Optional(NonEmptyString), caps: Type.Optional(Type.Array(NonEmptyString)), diff --git a/src/gateway/server-methods/nodes.ts b/src/gateway/server-methods/nodes.ts index e03f02393..971220d9d 100644 --- a/src/gateway/server-methods/nodes.ts +++ b/src/gateway/server-methods/nodes.ts @@ -42,6 +42,8 @@ export const nodeHandlers: GatewayRequestHandlers = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; caps?: string[]; @@ -55,6 +57,8 @@ export const nodeHandlers: GatewayRequestHandlers = { displayName: p.displayName, platform: p.platform, version: p.version, + coreVersion: p.coreVersion, + uiVersion: p.uiVersion, deviceFamily: p.deviceFamily, modelIdentifier: p.modelIdentifier, caps: p.caps, @@ -215,6 +219,8 @@ export const nodeHandlers: GatewayRequestHandlers = { displayName: live?.displayName ?? paired?.displayName, platform: live?.platform ?? paired?.platform, version: live?.version ?? paired?.version, + coreVersion: live?.coreVersion ?? paired?.coreVersion, + uiVersion: live?.uiVersion ?? paired?.uiVersion, deviceFamily: live?.deviceFamily ?? paired?.deviceFamily, modelIdentifier: live?.modelIdentifier ?? paired?.modelIdentifier, remoteIp: live?.remoteIp ?? paired?.remoteIp, @@ -275,6 +281,8 @@ export const nodeHandlers: GatewayRequestHandlers = { displayName: live?.displayName ?? paired?.displayName, platform: live?.platform ?? paired?.platform, version: live?.version ?? paired?.version, + coreVersion: live?.coreVersion ?? paired?.coreVersion, + uiVersion: live?.uiVersion ?? paired?.uiVersion, deviceFamily: live?.deviceFamily ?? paired?.deviceFamily, modelIdentifier: live?.modelIdentifier ?? paired?.modelIdentifier, remoteIp: live?.remoteIp ?? paired?.remoteIp, diff --git a/src/gateway/server-node-bridge.ts b/src/gateway/server-node-bridge.ts index 46897656d..20f9ee78b 100644 --- a/src/gateway/server-node-bridge.ts +++ b/src/gateway/server-node-bridge.ts @@ -43,6 +43,25 @@ export async function startGatewayNodeBridge(params: { }): Promise { const nodePresenceTimers = new Map>(); + const formatVersionLabel = (raw: string): string => { + const trimmed = raw.trim(); + if (!trimmed) return raw; + if (trimmed.toLowerCase().startsWith("v")) return trimmed; + return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed; + }; + + const resolveNodeVersionLabel = (node: { + coreVersion?: string; + uiVersion?: string; + }): string | null => { + const core = node.coreVersion?.trim(); + const ui = node.uiVersion?.trim(); + const parts: string[] = []; + if (core) parts.push(`core ${formatVersionLabel(core)}`); + if (ui) parts.push(`ui ${formatVersionLabel(ui)}`); + return parts.length > 0 ? parts.join(" · ") : null; + }; + const stopNodePresenceTimer = (nodeId: string) => { const timer = nodePresenceTimers.get(nodeId); if (timer) { @@ -57,6 +76,8 @@ export async function startGatewayNodeBridge(params: { displayName?: string; remoteIp?: string; version?: string; + coreVersion?: string; + uiVersion?: string; platform?: string; deviceFamily?: string; modelIdentifier?: string; @@ -66,7 +87,7 @@ export async function startGatewayNodeBridge(params: { const host = node.displayName?.trim() || node.nodeId; const rawIp = node.remoteIp?.trim(); const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined; - const version = node.version?.trim() || "unknown"; + const version = resolveNodeVersionLabel(node) ?? node.version?.trim() ?? "unknown"; const platform = node.platform?.trim() || undefined; const deviceFamily = node.deviceFamily?.trim() || undefined; const modelIdentifier = node.modelIdentifier?.trim() || undefined; diff --git a/src/infra/bridge/server/connection.ts b/src/infra/bridge/server/connection.ts index b0cd41f39..cf1028bbd 100644 --- a/src/infra/bridge/server/connection.ts +++ b/src/infra/bridge/server/connection.ts @@ -168,6 +168,8 @@ export function createNodeBridgeConnectionHandler(params: { displayName: verified.node.displayName ?? hello.displayName, platform: verified.node.platform ?? hello.platform, version: verified.node.version ?? hello.version, + coreVersion: verified.node.coreVersion ?? hello.coreVersion, + uiVersion: verified.node.uiVersion ?? hello.uiVersion, deviceFamily: verified.node.deviceFamily ?? hello.deviceFamily, modelIdentifier: verified.node.modelIdentifier ?? hello.modelIdentifier, caps, @@ -181,6 +183,8 @@ export function createNodeBridgeConnectionHandler(params: { displayName: nodeInfo.displayName, platform: nodeInfo.platform, version: nodeInfo.version, + coreVersion: nodeInfo.coreVersion, + uiVersion: nodeInfo.uiVersion, deviceFamily: nodeInfo.deviceFamily, modelIdentifier: nodeInfo.modelIdentifier, remoteIp: nodeInfo.remoteIp, @@ -243,6 +247,8 @@ export function createNodeBridgeConnectionHandler(params: { displayName: req.displayName, platform: req.platform, version: req.version, + coreVersion: req.coreVersion, + uiVersion: req.uiVersion, deviceFamily: req.deviceFamily, modelIdentifier: req.modelIdentifier, caps: Array.isArray(req.caps) @@ -286,6 +292,8 @@ export function createNodeBridgeConnectionHandler(params: { displayName: req.displayName, platform: req.platform, version: req.version, + coreVersion: req.coreVersion, + uiVersion: req.uiVersion, deviceFamily: req.deviceFamily, modelIdentifier: req.modelIdentifier, caps: Array.isArray(req.caps) ? req.caps.map((c) => String(c)).filter(Boolean) : undefined, diff --git a/src/infra/bridge/server/types.ts b/src/infra/bridge/server/types.ts index dada0ba3b..1b3be129a 100644 --- a/src/infra/bridge/server/types.ts +++ b/src/infra/bridge/server/types.ts @@ -9,6 +9,8 @@ export type BridgeHelloFrame = { token?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; caps?: string[]; @@ -22,6 +24,8 @@ export type BridgePairRequestFrame = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; caps?: string[]; @@ -113,6 +117,8 @@ export type NodeBridgeClientInfo = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; remoteIp?: string; diff --git a/src/infra/node-pairing.ts b/src/infra/node-pairing.ts index ac0420c08..de03f6817 100644 --- a/src/infra/node-pairing.ts +++ b/src/infra/node-pairing.ts @@ -9,6 +9,8 @@ export type NodePairingPendingRequest = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; caps?: string[]; @@ -26,6 +28,8 @@ export type NodePairingPairedNode = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; caps?: string[]; @@ -186,6 +190,8 @@ export async function requestNodePairing( displayName: req.displayName, platform: req.platform, version: req.version, + coreVersion: req.coreVersion, + uiVersion: req.uiVersion, deviceFamily: req.deviceFamily, modelIdentifier: req.modelIdentifier, caps: req.caps, @@ -219,6 +225,8 @@ export async function approveNodePairing( displayName: pending.displayName, platform: pending.platform, version: pending.version, + coreVersion: pending.coreVersion, + uiVersion: pending.uiVersion, deviceFamily: pending.deviceFamily, modelIdentifier: pending.modelIdentifier, caps: pending.caps, @@ -278,6 +286,8 @@ export async function updatePairedNodeMetadata( displayName: patch.displayName ?? existing.displayName, platform: patch.platform ?? existing.platform, version: patch.version ?? existing.version, + coreVersion: patch.coreVersion ?? existing.coreVersion, + uiVersion: patch.uiVersion ?? existing.uiVersion, deviceFamily: patch.deviceFamily ?? existing.deviceFamily, modelIdentifier: patch.modelIdentifier ?? existing.modelIdentifier, remoteIp: patch.remoteIp ?? existing.remoteIp, diff --git a/src/node-host/bridge-client.ts b/src/node-host/bridge-client.ts index 3d6cc1745..4d7e9e7c4 100644 --- a/src/node-host/bridge-client.ts +++ b/src/node-host/bridge-client.ts @@ -27,6 +27,8 @@ export type BridgeClientOptions = { displayName?: string; platform?: string; version?: string; + coreVersion?: string; + uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; caps?: string[]; @@ -172,6 +174,8 @@ export class BridgeClient { displayName: this.opts.displayName, platform: this.opts.platform, version: this.opts.version, + coreVersion: this.opts.coreVersion, + uiVersion: this.opts.uiVersion, deviceFamily: this.opts.deviceFamily, modelIdentifier: this.opts.modelIdentifier, caps: this.opts.caps, @@ -188,6 +192,8 @@ export class BridgeClient { displayName: this.opts.displayName, platform: this.opts.platform, version: this.opts.version, + coreVersion: this.opts.coreVersion, + uiVersion: this.opts.uiVersion, deviceFamily: this.opts.deviceFamily, modelIdentifier: this.opts.modelIdentifier, caps: this.opts.caps, diff --git a/src/node-host/runner.ts b/src/node-host/runner.ts index 1b186d7fe..7cc5f7e5d 100644 --- a/src/node-host/runner.ts +++ b/src/node-host/runner.ts @@ -346,6 +346,7 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise { displayName, platform: process.platform, version: VERSION, + coreVersion: VERSION, deviceFamily: os.platform(), modelIdentifier: os.hostname(), caps: ["system"],