Skip to content

TypeScript API Reference

The TypeScript bridge — type-safe passage between Mac and VM, because runtime errors at 3 AM are a lifestyle choice

The TypeScript config module at command-center/lib/config.ts provides typed access to the Sanctum instance configuration. It reads from the same ~/.sanctum/instance.yaml file as the shell library and is used by the command center dashboard and any Node.js tooling.

Same YAML. Same config. Same haus. But now with type safety, because if your haus is going to be managed by code, that code should at least know the difference between a string and a number. The shell library makes no such promises.

import {
get,
getConfig,
isEnabled,
expand,
slug,
name,
vmSsh,
whoami,
isNode,
myNodeType,
nodeGet,
nodeSsh,
nodeSshTs,
nodeVmSsh,
getNodes,
getNodesByType,
isNodeOnline,
isNodeServiceEnabled,
nodeService,
nodeHub,
getSatellites,
} from './lib/config.js';

Read any value from the instance config by dot-notation path. The optional second argument is the fallback returned when the path is missing — so get hands you a T, never an undefined you forgot to guard.

function get<T = string>(path: string, fallback?: T): T
ParameterTypeDescription
pathstringDot-delimited path into the config
fallbackTValue returned if the path is absent (default undefined)
const port = get('services.dashboard.port', 3001);
// 3333
const vmIp = get('network.vm_ip');
// "10.0.0.10"
const tz = get('instance.timezone');
// "America/New_York"

Return the entire parsed configuration object. The whole thing. Every detail your haus knows about itself, in one typed object.

function getConfig(): SanctumConfig
const config = getConfig();
console.log(config.instance.name);
// "Manoir Neo"

Check whether services.<name>.enabled is true. You pass the bare service name — isEnabled prefixes the services. and suffixes the .enabled for you. Pass the full path and it goes looking for services.services.x.enabled, finds nothing, and politely returns false.

function isEnabled(service: string): boolean
if (isEnabled('signal_proxy')) {
console.log('Signal bridge proxy is active');
}
if (isEnabled('voice_agent')) {
// ...
}

Expand a leading ~ to the configured Mac home directory (users.mac.home). Just the tilde — no {{token}} interpolation, no environment substitution. It is the smallest possible path helper, and it knows it. Anything that doesn’t start with ~ comes back untouched.

function expand(path: string): string
const result = expand('~/Projects/openclaw-skills');
// "/Users/neo/Projects/openclaw-skills"

Return the instance slug.

function slug(): string
slug();
// "manoir-neo"

Return the human-readable instance name.

function name(): string
name();
// "Manoir Neo"

Return the VM SSH target as user@ip, assembled from users.vm.username and network.vm_ip. Not an SSH-config alias — the literal address you can hand to ssh.

function vmSsh(): string
vmSsh();
// "ubuntu@10.0.0.10"

Return the current node identity (read from ~/.sanctum/.node_id).

function whoami(): string
whoami();
// "manoir"

Check if the current machine matches a given node ID — a string compare against whoami(). Note it takes a node ID (manoir), not a node type (hub); the two are easy to confuse, and the type system won’t save you here.

function isNode(nodeId: string): boolean
if (isNode('manoir')) {
console.log('Running on the manoir hub');
}

Return the node type of the current machine — reads nodes.<whoami>.type.

function myNodeType(): string
myNodeType();
// "hub"

A plain string, not a sealed union. The convention is hub, satellite, mobile, or sensor — the kinds of places your haus exists — but the function returns whatever the YAML says. The type system doesn’t judge. It just relays.


Read a field for a specific node — sugar for get('nodes.<id>.<field>'), with the same optional fallback.

function nodeGet<T = string>(nodeId: string, field: string, fallback?: T): T
ParameterTypeDescription
nodeIdstringNode identifier (e.g., "manoir", "chalet")
fieldstringDot-delimited path within that node’s config
fallbackTValue returned if the field is absent
nodeGet('manoir', 'tailscale_ip');
// "100.0.0.20"
nodeGet('chalet', 'type');
// "satellite"
nodeGet('manoir', 'host');
// "192.0.2.10"

Return the full nodes dict, keyed by node ID. For just the IDs, take the keys.

function getNodes(): Record<string, Record<string, unknown>>
Object.keys(getNodes());
// ["manoir", "chalet", "mbp", "ruview_front",
// "ruview_basement", "ruview_garage", "ruview_living"]

Return nodes filtered by type, each as its full config object with the id folded back in.

function getNodesByType(type: string): Array<{ id: string } & Record<string, unknown>>
getNodesByType('hub');
// [{ id: "manoir", type: "hub", display_name: "Manoir Neo", ... }]
getNodesByType('satellite').map((n) => n.id);
// ["chalet"]

Convenience wrapper for getNodesByType('satellite') — all satellite nodes as config objects.

function getSatellites(): Array<{ id: string } & Record<string, unknown>>
getSatellites().map((n) => n.id);
// ["chalet"]

Return the hub node ID that a given satellite syncs with — reads nodes.<id>.sync.hub. It answers “who does this satellite report home to,” not “which node is the hub.” Subtle difference, load-bearing in a two-haus topology.

function nodeHub(nodeId: string): string
nodeHub('chalet');
// "manoir"

Read the static nodes.<id>.online flag from config. Synchronous, no Promise, no network — it reports what the YAML last declared, not what the wire is doing right now.

function isNodeOnline(nodeId: string): boolean
if (isNodeOnline('chalet')) {
console.log('Chalet is marked online');
}

Return the SSH connection string for a node over LAN.

function nodeSsh(nodeId: string): string
nodeSsh('manoir');
// "neo@192.0.2.10"

Return the SSH connection string for a node over Tailscale.

function nodeSshTs(nodeId: string): string
nodeSshTs('chalet');
// "neo@100.0.0.21"

Return the SSH connection string for the VM on a specific node.

function nodeVmSsh(nodeId: string): string
nodeVmSsh('manoir');
// "ubuntu@10.0.0.10"

Check if a service is enabled on a specific node.

function isNodeServiceEnabled(nodeId: string, service: string): boolean
isNodeServiceEnabled('chalet', 'home_assistant');
// true
isNodeServiceEnabled('chalet', 'vm');
// false

Get a service field for a node — sugar for nodeGet('<id>', 'services.<svc>.<field>'), fallback param and all.

function nodeService<T = string>(nodeId: string, service: string, field: string, fallback?: T): T
nodeService('chalet', 'home_assistant', 'port');
// 8123

The command center dashboard serves the instance config (with secrets excluded) at the /api/config endpoint:

server/index.ts
import { getConfig } from '../lib/config.js';
app.get('/api/config', (_req, res) => {
const cfg = getConfig();
// Serve everything except secrets
const { secrets: _s, ...safe } = cfg;
res.json(safe);
});

vite.config.ts reads its dev-server port (and the proxy targets) from the config module rather than hardcoding them — so the dashboard listens where instance.yaml says, with a sane default if the key is missing:

vite.config.ts
import { get } from './lib/config.js';
const dashboardPort = get('services.dashboard.port', 3001);
// ...
server: { port: dashboardPort };