skill / standalone-service
!

Not installable via adompkg

This skill has no published release. adompkg install kyle/standalone-service will not work until a maintainer publishes a tarball with install.sh and uninstall.sh.

See the publishing docs for the package.json schema and tarball layout required to ship this skill.

Creating Standalone Adom Services

A standalone service runs on its own dedicated lightweight container, separate from user containers. Use this pattern when:

  • The service requires a large dataset (hundreds of MB+) that shouldn't be duplicated per container
  • The service needs a persistent server process (HTTP API, background jobs, scheduled updates)
  • Multiple containers should share the same data and service instance

Architecture Overview

Each service lives in its own GitHub repo (not in gallia) and runs on a lightweight container (sshd-only, ~7MB RAM idle). Users access it via a Rust CLI that gets installed alongside adom-cli.

Dev Container                 GitHub                     Service Container (default-light)
  service repo dev         --> push --> adom-inc/service-X --> SSH in, git pull
                                                                 |
                                                              node server.js (port XXXX)
                                                                 |
User Containers ------------- Rust CLI (HTTP) ------------------>|

Step 1: Choose Access Pattern

Ask the user which access pattern they want:

Option A: Rust CLI (recommended for new services)

A compiled Rust binary that wraps the service HTTP API. Installed alongside adom-cli on every user container.

User Container                    Service Container
  Claude --> CLI command           HTTP API (port XXXX)
    |                                ^
  service-cli search "query"         |
    |                                |
  HTTP request ------------------->  |
    |
  pushToViewer() --> AV

Best when:

  • The API has multiple endpoints
  • You want zero-overhead on user containers (no background process)
  • You want consistent UX with adom-cli

Option B: Skill-driven HTTP (e.g., KiCad CLI -- services/kicad-cli)

Skills teach Claude the HTTP endpoints directly. Claude uses curl or fetch.

User Container                    Service Container
  Claude --> reads skill           HTTP API (port XXXX)
    |                                ^
  curl via Bash ------- HTTP ------> |

Best when:

  • The API surface is small and stable (handful of endpoints)
  • The service is primarily used by one or two skills
  • You want fewer moving parts (no CLI to build)

Option C: MCP Server (legacy pattern)

MCP stdio process on each user container proxying to the remote API. Not recommended for new services -- use Rust CLI instead. Legacy MCP services (JLCPCB, Mouser, DigiKey, Wiki) still work but won't be the pattern going forward.

Rust CLI Skill-driven HTTP MCP Server (legacy)
User container overhead Zero (compiled binary) Zero Node.js stdio process
Discovery CLI skill + --help Requires skill activation Automatic MCP tools
Validation CLI argument parsing Relies on skill instructions JSON Schema
Distribution Binary pulled during install Skill deployed by gallia MCP server.js in gallia
Reference adom-cli services/kicad-cli jlcpcb/mcp/

Step 2: Create the Service GitHub Repo

Each service gets its own repo at adom-inc/service-<name>. This keeps gallia focused on user-container tooling.

Create the repo on GitHub (or via the Adom API):

# Via GitHub CLI (if authenticated)
gh repo create adom-inc/service-<name> --private --description "Adom <name> service"

# Or via Adom API
API_KEY=$(cat /var/run/adom/api-key)
curl -s -X POST 'https://carbon.adom.inc/user/repos' \
  -b "session_token=$API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"service-<name>","description":"<description>","private":true}'

Step 3: Write the Service Code

Structure the repo like this:

service-<name>/
  server.js          # HTTP API server
  package.json       # Dependencies
  service.json       # Auto-start manifest
  start.sh           # Idempotent start script
  setup.sh           # First-time setup (install deps, init DB, etc.)
  README.md          # Service documentation

service.json -- Service Manifest

This is read by auto-start and watchdog scripts. It's the single source of truth for how to run the service.

{
  "name": "my-service",
  "service": "my-tag",
  "description": "What this service does",
  "port": 8XXX,
  "health": "http",
  "start": "node server.js",
  "cwd": ".",
  "log": "/tmp/my-service.log"
}
Field Required Description
name yes Service name (used in logs and watchdog output)
service yes Service filter tag (see Service Filtering below)
description no Human-readable description
port yes Port the service listens on
health yes "http" (GET /health returns 200) or "tcp" (port open check)
start yes Shell command to start the service
cwd yes Working directory -- "." means same dir as service.json
log no Log file path (defaults to /tmp/<name>.log)

server.js -- HTTP API Server

Use Node.js built-in http module (no Express). Key patterns:

import { createServer } from 'http';

const PORT = parseInt(process.env.MY_SERVICE_PORT || '8XXX', 10);

const server = createServer(async (req, res) => {
  const url = new URL(req.url, `http://localhost:${PORT}`);

  // Health endpoint (required)
  if (url.pathname === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true, service: 'my-service', uptime: process.uptime() }));
    return;
  }

  // Landing page (serve HTML when browser visits)
  if (url.pathname === '/' && req.headers.accept?.includes('text/html')) {
    const proto = req.headers['x-forwarded-proto'] || 'https';
    const host = req.headers['x-forwarded-host'] || req.headers.host;
    const baseUrl = `${proto}://${host}`;
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(landingPageHTML(baseUrl));
    return;
  }

  // Your API endpoints here...
});

server.listen(PORT, '0.0.0.0', () => {
  console.log(`[my-service] Running on port ${PORT}`);
});

package.json -- Standalone Dependencies

{
  "name": "service-my-service",
  "version": "1.0.0",
  "type": "module",
  "private": true,
  "dependencies": { },
  "scripts": { "start": "node server.js" }
}

start.sh -- Idempotent Start Script

Always the same 3-step pattern:

#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_FILE="/tmp/my-service.log"
PORT=8XXX

# 1. Check if already running (idempotent)
if curl -sf --max-time 2 http://127.0.0.1:${PORT}/health > /dev/null 2>&1; then
  echo "[my-service] Server already running on port ${PORT}"
  exit 0
fi

# 2. Install deps if needed
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
  cd "$SCRIPT_DIR" && npm install --production 2>&1 | tail -3
fi

# 3. Start with nohup
cd "$SCRIPT_DIR"
nohup /usr/bin/node server.js >> "$LOG_FILE" 2>&1 &
echo "[my-service] Server started (PID $!), logging to $LOG_FILE"

Important: Use /usr/bin/node (explicit path) -- PATH may not be set at boot time.

setup.sh -- First-Time Setup

Run once after cloning the repo on the service container:

#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

echo "[setup] Installing dependencies..."
cd "$SCRIPT_DIR" && npm install --production

# Database initialization (if needed)
# node setup-db.js

# Set GALLIA_SERVICE so auto-start knows what to run
echo "GALLIA_SERVICE=my-tag" | sudo tee -a /etc/environment
export GALLIA_SERVICE=my-tag

echo "[setup] Starting service..."
bash "$SCRIPT_DIR/start.sh"

echo "[setup] Verifying health..."
sleep 2
curl -sf http://127.0.0.1:${PORT}/health && echo " OK" || echo " FAILED"

Step 4: Create a Lightweight Container

Use adom-cli to create a default-light container (sshd-only).

Important: Containers must be associated with a repo at creation time -- this cannot be changed later. Always create the repo first (Step 2), get its ID, then pass --repo-id.

# Get the Adom repo ID (from Step 2, or look it up)
adom-cli carbon user repos  # find the "id" field for your service repo

# Create container with repo association
adom-cli carbon containers create \
  --image-id 69b43b3e58d13e5ce628cdc5 \
  --class small \
  --ssh \
  --repo-id <ADOM_REPO_ID>

Note: --repo-id takes the Adom repo ID (from carbon.adom.inc), not a GitHub repo ID.

When associated with a repo, the SSH username includes the repo name (e.g. john-service-wiki-rk6euj7525tq), making it easy to identify. Without --repo-id, the container is orphaned with a random slug and won't appear in repo container listings.

Note the SSH credentials from the response:

{
  "ssh_credentials": {
    "command": "ssh [email protected]",
    "hostname": "adom.cloud",
    "port": 22,
    "username": "john-service-wiki-rk6euj7525tq"
  }
}

Prerequisites: You need an SSH key registered with your Adom account. If you don't have one:

# Generate key if missing
[ ! -f ~/.ssh/id_ed25519 ] && ssh-keygen -t ed25519 -C "adom" -f ~/.ssh/id_ed25519 -N ""

# Register if none on Adom
[ "$(adom-cli carbon user ssh-keys)" = "[]" ] && \
  adom-cli carbon user ssh-key-add --display-name "auto" "$(cat ~/.ssh/id_ed25519.pub)"

Step 5: Deploy the Service

SSH into the container, clone the repo, and run setup:

# SSH in
ssh -o StrictHostKeyChecking=accept-new [email protected]

# Clone the service repo (GitHub auth may be needed)
cd ~ && git clone https://github.com/adom-inc/service-<name>.git service

# Run first-time setup
cd service && bash setup.sh

The default-light image includes node, git, python3, and curl. If your service needs additional tools, install them in setup.sh.

Add a cron @reboot for auto-start

(crontab -l 2>/dev/null; echo "@reboot cd /home/adom/service && bash start.sh >> /tmp/my-service-boot.log 2>&1") | crontab -

Expose a public URL (optional)

If the service needs to be reachable from other containers:

# From your dev container (not from inside the service container)
adom-cli carbon containers port-add <slug> --port 8XXX --prefix service-name

This creates a public URL like service-name-abc123.adom.cloud.

Step 6: Wire Up Consumer Access

Back on the dev container, make the service accessible to all users.

For Rust CLI services (Option A)

  1. Create a Rust CLI project that wraps the HTTP API (follow adom-cli patterns)
  2. Add the CLI binary to gallia's install.mjs so it gets pulled during installation
  3. Create a skill at gallia/skills/<service-name>/SKILL.md documenting the CLI commands

For skill-driven services (Option B)

Document the HTTP endpoints in a gallia skill. Include the service's public URL:

## API Endpoints

Base URL: `https://service-name-abc123.adom.cloud`

### Search
GET /search?q=<query>&limit=10

Hardcode the service URL

Whether in a CLI or skill, hardcode the public URL as the default:

https://<service-url>.adom.cloud/

Create a port mapping with adom-cli carbon containers port-add. For example:

https://service-name-abc123.adom.cloud

Service Filtering (GALLIA_SERVICE)

The GALLIA_SERVICE environment variable (set in /etc/environment) tells auto-start scripts which services to manage on this container.

How filtering works

GALLIA_SERVICE value What starts
(unset or empty) Defaults to "local" -- only services tagged "local"
"wiki" Only services tagged "wiki"
"local,wiki" Services tagged "local" OR "wiki"
"all" Every service regardless of tag

For service containers: Set GALLIA_SERVICE=<tag> in /etc/environment so only that service starts on boot.

For user containers: Leave unset. Defaults to "local".

Setting the env var

echo 'GALLIA_SERVICE=my-tag' | sudo tee -a /etc/environment

Deploying Updates

# From your dev container, SSH into the service container and update
ssh [email protected] "cd ~/service && git pull origin main && npm install && pkill -f 'node.*server.js'; bash start.sh"

Or run each step interactively:

ssh [email protected]
cd ~/service
git pull origin main
npm install
pkill -f 'node.*server.js' || true
bash start.sh

Scheduled Maintenance (Cron)

For daily database updates or cleanup:

#!/bin/bash
# /etc/cron.daily/update-my-service-db
CURL_ARGS=(-sfL -o "$STAGING_PATH" --etag-save "$ETAG_FILE")
[ -f "$ETAG_FILE" ] && CURL_ARGS+=(--etag-compare "$ETAG_FILE")
HTTP_CODE=$(curl -w '%{http_code}' "${CURL_ARGS[@]}" "$DB_URL" 2>/dev/null || true)
if [ "$HTTP_CODE" = "304" ]; then exit 0; fi
mv "$STAGING_PATH" "$DB_PATH"
pkill -HUP -f "node server.js"  # zero-downtime reload

Adom Viewer Integration

If your service returns results that benefit from rich display, register it in the AV Service Dashboard.

Edit /home/adom/gallia/viewer/viewer/index.html:

  1. Add an entry to the SERVICE_REGISTRY JS object with: name, icon, desc, container, port, repo, repoPath, mcpName, and optionally local: true
  2. Add an <option value="svc-<key>"> to the <optgroup> "Standalone Services"
  3. Add a skill-card <div> to the About page

The health check, URL generation, and dashboard rendering are all automatic from the registry entry.

Testing

Create test.js in the service repo:

  • Hit local HTTP API directly (use 127.0.0.1, not localhost)
  • Push styled HTML results to Adom Viewer
  • Assert count > 0 before filter assertions -- .every() on empty array returns true
  • Test with real user queries, not just exact IDs

Reference Implementations

Service Repo Pattern Container Image Notes
JLCPCB gallia (legacy) MCP (legacy) default-vscode SQLite DB, daily cron
Mouser gallia (legacy) MCP (legacy) default-vscode HTTP proxy to Mouser API
DigiKey gallia (legacy) MCP (legacy) default-vscode OAuth client_credentials
KiCad CLI gallia (legacy) Skill-driven HTTP default-vscode Heavy install (~600MB KiCad 9)
Wiki gallia (legacy) MCP (legacy) default-vscode SQLite + FTS5, asset uploads
New services adom-inc/service-X Rust CLI default-light Recommended pattern

Troubleshooting

Symptom Cause Fix
SSH "Permission denied" No SSH key registered Generate and register: ssh-keygen && adom-cli carbon user ssh-key-add
Service starts but health check fails Wrong port Match port in service.json to what server.js listens on
Service doesn't survive reboot No auto-start Add @reboot cron entry (see Step 5)
npm install fails Missing system deps Install via sudo apt-get install <pkg> in setup.sh
Can't access from user container No port mapping or wrong URL Add port mapping: adom-cli carbon containers port-add
Container not found after creation Provisioning delay Wait 15-30 seconds, then retry SSH